I am trying to convert this class based component into a function component. For some reason the Query (setQ) is not being read by the handleFormSubmit function. When I make the param in the setQ an object the value of the input form becomes [object, Object]. What am I doing wrong?
class component
import React, { Component } from "react";
import Jumbotron from "../components/Jumbotron";
import Card from "../components/Card";
import Form from "../components/Form";
import Book from "../components/Book";
import Footer from "../components/Footer";
import API from "../utils/API";
import { Col, Row, Container } from "../components/Grid";
import { List } from "../components/List";
class Home extends Component {
state = {
books: [],
q: "",
message: "Search For A Book To Begin!"
};
handleInputChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
getBooks = () => {
API.getBooks(this.state.q)
.then(res =>
this.setState({
books: res.data
})
)
.catch(() =>
this.setState({
books: [],
message: "No New Books Found, Try a Different Query"
})
);
};
handleFormSubmit = event => {
event.preventDefault();
this.getBooks();
};
handleBookSave = id => {
const book = this.state.books.find(book => book.id === id);
API.saveBook({
googleId: book.id,
title: book.volumeInfo.title,
subtitle: book.volumeInfo.subtitle,
link: book.volumeInfo.infoLink,
authors: book.volumeInfo.authors,
description: book.volumeInfo.description,
image: book.volumeInfo.imageLinks.thumbnail
}).then(() => this.getBooks());
};
render() {
return (
<Container>
<Row>
<Col size="md-12">
<Jumbotron>
<h1 className="text-center">
<strong>(React) Google Books Search</strong>
</h1>
<h2 className="text-center">Search for and Save Books of Interest.</h2>
</Jumbotron>
</Col>
<Col size="md-12">
<Card title="Book Search" icon="far fa-book">
<Form
handleInputChange={this.handleInputChange}
handleFormSubmit={this.handleFormSubmit}
q={this.state.q}
/>
</Card>
</Col>
</Row>
<Row>
<Col size="md-12">
<Card title="Results">
{this.state.books.length ? (
<List>
{this.state.books.map(book => (
<Book
key={book.id}
title={book.volumeInfo.title}
subtitle={book.volumeInfo.subtitle}
link={book.volumeInfo.infoLink}
authors={book.volumeInfo.authors.join(", ")}
description={book.volumeInfo.description}
image={book.volumeInfo.imageLinks.thumbnail}
Button={() => (
<button
onClick={() => this.handleBookSave(book.id)}
className="btn btn-primary ml-2"
>
Save
</button>
)}
/>
))}
</List>
) : (
<h2 className="text-center">{this.state.message}</h2>
)}
</Card>
</Col>
</Row>
<Footer />
</Container>
);
}
}
export default Home;
functional conversion
import React from "react";
// import Jumbotron from "react-bootstrap/Jumbotron";
import Row from "react-bootstrap/Row";
import Card from "../components/Card";
import Form from "../components/Form";
import Col from "react-bootstrap/Col";
import Container from "react-bootstrap/Container";
import Jumbotron from "react-bootstrap/Jumbotron";
import { useState } from "react";
import API from "../utils/API";
import Book from "../components/Book";
import Button from "react-bootstrap/Button";
import { List } from "../components/List";
import Footer from "../components/Footer";
import "./style.css";
export default function Home() {
let [books, setBooks] = useState([]);
let [q, setQ] = useState("");
let [message, setMessage] = useState("Search For A Book to Begin");
const handleInputChange = (event) => {
let { name, value } = event.target;
setQ(([name] = value));
};
let getBooks = () => {
API.getBooks(q)
.then((res) => setBooks(res.data))
.catch(() => setBooks([]));
setMessage("No New Books Found, Try a Different Query");
};
const handleFormSubmit = (event) => {
event.preventDefault();
getBooks();
};
let handleBookSave = (id) => {
const book = books.find((book) => book.id === id);
API.saveBook({
googleId: book.id,
title: book.volumeInfo.title,
subtitle: book.volumeInfo.subtitle,
link: book.volumeInfo.infoLink,
authors: book.volumeInfo.authors,
description: book.volumeInfo.description,
image: book.volumeInfo.imageLinks.thumbnail,
}).then(() => getBooks());
};
return (
<div>
<Container>
<Row>
<Col md={12}>
<Jumbotron className="rounded-3 mt-4">
<h1 className="text-center ">
<strong>(React) Google Books Search</strong>
</h1>
<h2 className="text-center">
Search for and Save Books of Interest.
</h2>
</Jumbotron>
</Col>
<Col md={12}>
<Card title="Book Search" icon=" fa-book">
<Form
handleInputChange={handleInputChange}
handleFormSubmit={handleFormSubmit}
q={q}
/>
</Card>
</Col>
</Row>
<Row>
<Col md={12}>
<Card title="Results">
{books.length ? (
<List>
{books.map((book) => (
<Book
key={book.id}
title={book.volumeInfo.title}
subtitle={book.volumeInfo.subtitle}
link={book.volumeInfo.infolink}
authors={book.volumeInfo.authors.join(", ")}
description={book.volumeInfo.description}
image={book.volumeInfo.imageLinks.thumbnail}
Btn={() => (
<Button
onClick={() => handleBookSave(book.id)}
variant="primary"
className="ml-2"
>
Save
</Button>
)}
/>
))}
</List>
) : (
<h2 className="text-center">{message}</h2>
)}
</Card>
</Col>
</Row>
<Footer />
</Container>
</div>
);
}
I'm not sure exactly what you want here, but I see the problem.
First let's look here:
state = {
books: [],
q: "",
message: "Search For A Book To Begin!"
};
handleInputChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
In a class component, state is an object with properties. You are taking advantage of that fact to allow accessing individual pieces of state with a dynamic name, and then setting that to value.
Now let's look at the functional component:
let [books, setBooks] = useState([]);
let [q, setQ] = useState("");
let [message, setMessage] = useState("Search For A Book to Begin");
const handleInputChange = (event) => {
let { name, value } = event.target;
setQ(([name] = value));
};
Note that you no longer have a single state object. You have three completely separate pieces of state. This handleInputChange function only sets one of them, q.
And [name] = value is especially strange. What that's actually doing is assigning the first character of the string in value to the variable name, which is never used or read from again. That is certainly not what you want.
In order to do what I think you want you have to call each setMyStateHere() setter explicitly.
So I think you want this:
const handleInputChange = (event) => {
let { name, value } = event.target;
if (name === 'q') {
setQ(value)
} else if (name === 'message') {
setMessage(value)
} else {
throw new Error(`Unhandled input name: ${name}`) // or something
}
};
Here you inspect name to figure out which state to set, and then call the right setter function.
Or perhaps, if you want it to closer to your class component, you can store an object in state:
let [books, setBooks] = useState([]);
let [formValues, setFormValues] = useState({
q: "",
message: "Search For A Book to Begin"
);
const handleInputChange = (event) => {
let { name, value } = event.target;
setFormValues({ ...formValues, [name]: value })
}
Now you have an object in state like you had before, and can dynamically set properties on that object.
All this assumes that you want to set something besides q from this form, otherwise this becomes trivially just:
const handleInputChange = event => { setQ(event.target.value) }
Related
I'm making a todo list app using react and firebase realtime database.
I want to get the todos ordered by date.
My Database:
And if I cant do this from firebase, is there a way to order it from the client side (react)?
My Code
Todos.js:
import { useState, useEffect } from "react";
import { signOut, onAuthStateChanged } from "firebase/auth";
import { uid } from "uid";
import { set, ref, onValue } from "firebase/database";
import { auth, db } from "../firebase";
import moment from "moment";
function Todos() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState("");
const navigate = useNavigate();
useEffect(() => {
auth.onAuthStateChanged((user) => {
if (user) {
onValue(ref(db, `/${auth.currentUser.uid}`), (snapshot) => {
setTodos([]);
const data = snapshot.val();
if (data !== null) {
Object.values(data).map((todo) => {
setTodos((currentTodos) => [todo, ...currentTodos]);
});
}
});
} else {
navigate("/");
}
});
}, []);
const handleSignOut = () => {
signOut(auth)
.then(() => navigate("/"))
.catch((error) => alert(error.message));
};
const addTodo = () => {
const uidd = uid();
set(ref(db, `${auth.currentUser.uid}/${uidd}`), {
task: newTodo,
uid: uidd,
createdAt: moment().format("YYYY-MM-DD k:m:s"),
});
setNewTodo("");
};
return (
<>
<Center>
<Button colorScheme="red" marginTop={5} onClick={handleSignOut}>
Logout
</Button>
</Center>
<Container
maxW="4xl"
marginTop={8}
display="flex"
alignItems="center"
justifyContent="center"
>
<Box
boxShadow="base"
rounded="lg"
padding={10}
background="white"
width="100%"
>
<Heading as="h1" size="md" textAlign="center">
Todo List App
</Heading>
<form onSubmit={(e) => e.preventDefault()}>
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
marginTop={5}
>
<Input
placeholder="New Task"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
size="lg"
width="80%"
/>
<Button
colorScheme="teal"
height={45}
rightIcon={<MdAdd />}
margin={0}
onClick={addTodo}
type="submit"
>
Add
</Button>
</Box>
</form>
{todos.map((todo, index) => {
return <Todo key={index} task={todo.task} uid={todo.uid} />;
})}
</Box>
</Container>
</>
);
}
export default Todos;
Since you are loading the TODOs for a single user, you can indeed order them by their createdAt property. To do this, use a query as shown in the documentation on ordering and filtering data:
const ref = ref(db, `/${auth.currentUser.uid}`);
const query = query(ref, orderByChild('createdAt'));
onValue(query, (snapshot) => {
...
Inside the code you'll then need to make sure to use snapshot.forEach to loop over the children in order, as calling .val() before that will return a JSON object and the properties in a JSON object are by definition not ordered:
snapshot.forEach((child) => {
console.log(child.key, child.val());
});
Goal: Passing a function from parent to child that updates the values on parent component.
I've searched some other threads that were not using arrow functions that corrected their issue. I figure it is some sort of binding issue but I'm not sure where... This is sort of a dumbed down version of what I'm trying to do.
"react": "^18.1.0",
const Parent = () => {
const [value, setValue] = useState(0)
const update = () => {
setValue(value + 1)
}
return (
<>
{value}
<Child update={update} />
</>
)
}
I've tried passing the function a few different ways from parent to child.
<Child update={() => update()} />
<Child update={setValue} />
<Child update={() => setValue(value + 1)} />
<Child value={value} setValue={setValue} />
... and so on
const Child = ({ update }) => {
const handle = event => {
event.preventDefault()
update()
}
return (
<form onSubmit={handle}>
</form>
)
}
console.log shows update is a function in child component, and even shows the correct values to be updated - however when it is time for the function to be called I get that error.
FULL CODE
parent:
import React, { useEffect, useState, useRef } from 'react'
//Style
import { Container, Card, Button, Alert, Row, Col, Form } from 'react-bootstrap'
//Authentication
import { useAuth } from '../../authentication/AuthContext'
//Navigation
import { Link, useNavigate } from 'react-router-dom'
//Components
import Navigation from '../../components/Navigation'
import Loading from '../../components/Loading'
import CreateHOA from '../../components/CreateHOA'
import MapHOA from '../../components/MapHOA'
//Requests
import { addUser } from '../../requests/addUser'
import { getUser } from '../../requests/getUser'
const Dashboard = () => {
const [error, setError] = useState()
const [loading, setLoading] = useState(true)
const [database, setDatabase] = useState(null)
const [view, setView] = useState()
const [action, setAction] = useState({
createHoa: true
})
const { currentUser, logout } = useAuth()
const navigate = useNavigate()
const update = async () => {
getUser(currentUser.uid)
.then(res => {
console.log('get user', res)
if(res.data){
console.log('user exists')
console.log('set database')
setDatabase(res.data[0])
try{
console.log('check hoa exists')
if(res.data.hoa.length > 0){
console.log('hoa exists; set action/view')
setAction({...action, createHoa: false })
setView(res.data.hoa[0])
}
}catch(e){
console.log('hoa doesnt exist')
}
}else{
console.log('user doesnt exist')
addUser({ uid: currentUser.uid})
.then(res => {
console.log('add user', res)
console.log('set database')
setDatabase({ uid: currentUser.uid })
})
}
})
.then(() => {
console.log('set loading to false')
setLoading(false)
})
}
useEffect(() => {
update()
}, [])
return (
<>
{loading ? <Loading /> : <>
<Navigation />
<br />
<Container className='white-bg'>
<Row>
<Col xl={12}>
<h3 className='white'>Dashboard</h3>
<br /><br />
</Col>
</Row>
{action.createHoa ?
<CreateHOA uid={currentUser.uid} update={update} /> :
<>{currentUser.uid}</>
}
</Container>
<div className='footer'>
footer
</div>
</>}
</>
)
}
export default Dashboard
child
import React, { useState, useRef } from 'react'
//Style
import { Container, Card, Button, Alert, Row, Col, Form } from 'react-bootstrap'
//Components
import LoadingSmall from '../LoadingSmall'
//Requests
import { addHoa } from '../../requests/addHoa'
const CreateHOA = (uid, { update }) => {
const [loading, setLoading] = useState(false)
const nameRef = useRef()
const submit = event => {
event.preventDefault()
setLoading(true)
console.log('UID', uid)
addHoa(uid, nameRef.current.value).then(res => {
console.log(res)
update();
})
}
return (
<Row>
<Col xl={12}>
<Card>
<Card.Header>Action Needed</Card.Header>
<Card.Body>
{loading ? <LoadingSmall /> : <>
<Card.Title>Create an HOA</Card.Title>
<Card.Text>
<p>Type in the name of your Home Owners Association below and click create to get started!</p>
<Form onSubmit={submit}>
<Form.Group id='name'>
<Form.Control type='text' ref={nameRef} required />
</Form.Group>
<br />
<Button type='submit'>Create</Button>
</Form>
</Card.Text>
</>}
</Card.Body>
</Card>
</Col>
</Row>
)
}
export default CreateHOA
Use this.setValue() instead of just setValue():
const Parent = () => {
const [value, setValue] = useState(0)
const update = () => {
this.setValue(value + 1)
}
return (
<>
{value}
<Child update={update} />
</>
)
}
I have a form where the user can add input fields with a click event. I built a handler where the user can add multiple text fields and insert multiple names that will be sent to a database.
I was successful in adding input fields, following this link
I ran into a challenge where I could not submit the string data to the database. I solved this issue by pushing the value of the input field to the employee array.
My problem: when I add a second text field a get the error employee.push is not a function.
My question how can I solve this? For example, if the user wants to add two names by adding two text fields to input the names, how can I fix the code to send the names to the DB?
I know I have to map the result of the original map to do this, but I am not sure how to insert the target value from the handler event in the employee array (where the name strings are stored in the DB).
It got the code to only work once, when you add one name and submit, the name is sent to the database, however when more than one name is added (even before the submit) the code breaks.
I am using context and useState hooks:
AddAccountsContxt.js
import React, { useState, createContext } from "react";
import axios from "axios";
export const AddAccountsContxt = createContext();
export const AddAccountsContxtProvider = (props) => {
const [company, setCompany] = useState("");
const [address, setAddress] = useState("");
const [phone, setPhone] = useState("");
const [details, setDetails] = useState([]);
const [employee, setEmployee] = useState([]);
const addAccountHandler = (e) => {
e.preventDefault();
setDetails([...details, employee]);
console.log('employee', details)
details.push({employee:employee});
axios({
method: "POST",
url: "http://localhost:5000/insert-form",
data: {
company: company,
address: address,
phone: phone,
// employee: employee,
details: details
},
});
};
return (
<>
<AddAccountsContxt.Provider
value={[
company,
setCompany,
address,
setAddress,
phone,
setPhone,
employee,
setEmployee,
details,
addAccountHandler,
]}
>
{props.children}
</AddAccountsContxt.Provider>
</>
);
};
EmployeeName.js
import { React, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { Col } from "react-bootstrap";
import { Row } from "react-bootstrap";
const EmployeeName = ({ employee, details, setEmployee}) => {
const [inputList, setInputList] = useState([{ employee: "" }]);
const handleInputChange = (e, index) => {
const { name, value } = e.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
console.log("value", value);
//below code implamented to push name string to employee array
setEmployee(...employee, value);
if (value) {
employee.push({value:value});
}
};
const handleRemoveClick = (index) => {
const list = [...inputList];
list.splice(index, 1);
setInputList(list);
};
const AddInputField = () => {
setInputList([...inputList, { employee: "" }]);
};
return (
<>
<Button onClick={AddInputField}>
click here to add company employees
</Button>
{inputList.length > 0 &&
inputList.map((x, i) => {
return (
<Row key={i}>
<Col sm={11}>
<Form.Group>
<Form.Control
placeholder="Add Employee Name"
onChange={(e) => handleInputChange(e, i)}
value={x.employee}
name="employee"
type="text"
/>
</Form.Group>
</Col>
<Col>
{inputList.length - 1 === i ? (
<Button
onClick={AddInputField}
> Add
</Button>
) : (
<Button onClick={() => handleRemoveClick(i)}>Remove</Button>
)}
</Col>
</Row>
);
})}
</>
);
};
export default EmployeeName;
In React, state should be treated as immutable, so you shouldn't even be pushing to elements to employee, but rather pass in an entirely new array to setEmployee.
When you call setEmployee(...employee, value) in handleInputChange of EmployeeName, you set employee as the first element of its previous value, which makes it no longer an array on its next render, causing the TypeError when attempting to push. Instead put a useEffect with a dependency on the inputList so that when the inputList is set, map the inputs to an employee.
const EmployeeName = ({ setEmployee }) => {
const [inputList, setInputList] = useState([{ employee: '' }]);
useEffect(() => setEmployee(inputList.map((input) => ({ value: input.employee }))), [inputList]);
const handleInputChange = (event, index) => {
const { name, value } = event.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
};
const handleRemoveClick = (index) => setInputList(inputList.filter((_, i) => i !== index));
const AddInputField = () => setInputList([...inputList, { employee: '' }]);
return (
<>
<Button onClick={AddInputField}>click here to add company employees</Button>
{inputList.map((input, index) => (
<Row key={index}>
<Col sm={11}>
<Form.Group>
<Form.Control
placeholder='Add Employee Name'
onChange={(event) => handleInputChange(event, index)}
value={input.employee}
name='employee'
type='text'
/>
</Form.Group>
</Col>
<Col>
{inputList.length - 1 === index ? (
<Button onClick={AddInputField}> Add</Button>
) : (
<Button onClick={() => handleRemoveClick(index)}>Remove</Button>
)}
</Col>
</Row>
))}
</>
);
};
This also applies to all other states like detail which should be rewritten to just setDetail([...details, {employee}] instead of attempting to push to state.
Hi I am creating an app where a user can search for a book and put it on a shelf depending on which shelf the user clicks on. Currently the user can type a query and many results can get displayed. I want the user to a dropdown on a book and click on a shelf (in the dropdown) to select that book and move it to that shelf.
What I trying to do now is retrieve the book object when the user clicks on a dropdown option (which is a shelf). I want to pass this book object into an api call. How would I retrieve the book object when the user clicks on a specific book's dropdown option? I understand that this might involve event bubbling.
I hope this question makes sense.
SearchPage.js
import React, { useEffect, useState } from 'react';
import { BsArrowLeftShort } from 'react-icons/bs';
import SearchBar from '../components/SearchBar';
import { search, update, getAll } from '../api/BooksAPI';
import Book from '../components/Book';
const SearchPage = () => {
const [query, setQuery] = useState('');
const [data, setData] = useState([]);
const handleChange = (e) => {
setQuery(e.target.value);
};
useEffect(() => {
const bookSearch = setTimeout(() => {
if (query.length > 0) {
search(query).then((res) => {
if (res.length > 0) {
setData(res);
} else setData([]);
});
} else {
setData([]);
}
}, 1000);
return () => clearTimeout(bookSearch);
}, [query]);
const [shelfType, setShelfType] = useState('None');
const [currentBook, setCurrentBook] = useState({});
const handleShelfTypeClick = (e) => {
setShelfType(e.target.value);
console.log(e.target.parentElement.parentElement.parentElement);
//here I want to retrieve the book object when the user clicks on a dropdown option (shelf)
};
return (
<div>
<SearchBar
type="text"
searchValue={query}
placeholder="Search for a book"
icon={<BsArrowLeftShort />}
handleChange={handleChange}
/>
<div className="book-list">
{data !== []
? data.map((book) => (
<Book
handleShelfTypeClick={handleShelfTypeClick}
book={book}
key={book.id}
/>
))
: 'ok'}
</div>
</div>
);
};
export default SearchPage;
Book.js
import React from 'react';
import PropTypes from 'prop-types';
import ButtonDropDown from './ButtonDropDown';
const Book = ({ book, handleShelfTypeClick }) => {
return (
<div className="book">
<img
src={book.imageLinks.thumbnail}
alt={book.title}
className="book-thumbnail"
/>
<ButtonDropDown
choices={['Currently Reading', 'Want to Read', 'Read', 'None']}
getShelfType={handleShelfTypeClick}
/>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
);
};
Book.propTypes = {
handleShelfTypeClick: PropTypes.func.isRequired,
book: PropTypes.shape({
imageLinks: PropTypes.shape({
thumbnail: PropTypes.string.isRequired,
}),
title: PropTypes.string.isRequired,
authors: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
};
export default Book;
ButtonDropDown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { BsFillCaretDownFill } from 'react-icons/bs';
const ButtonDropDown = ({ choices, label, getShelfType }) => {
const [active, setActive] = useState(false);
const toggleClass = () => {
setActive(!active);
};
return (
<div className="dropdown">
<button
type="button"
className="dropbtn"
onFocus={toggleClass}
onBlur={toggleClass}
>
<BsFillCaretDownFill />
</button>
<div
id="myDropdown"
className={`dropdown-content ${active ? `show` : `hide`}`}
>
<div className="dropdown-label">{label}</div>
{choices.map((choice, index) => (
<button
// eslint-disable-next-line react/no-array-index-key
key={index}
className="dropdown-choice"
onClick={getShelfType}
type="button"
value={choice}
>
{choice}
</button>
))}
</div>
</div>
);
};
ButtonDropDown.propTypes = {
choices: PropTypes.arrayOf(PropTypes.string).isRequired,
label: PropTypes.string,
getShelfType: PropTypes.func.isRequired,
};
ButtonDropDown.defaultProps = {
label: 'Move to...',
};
export default ButtonDropDown;
You are focusing on the signature for the onClick event, but you can actually pass a callback with any format that you need and then build onClick dinamically.
For instance, in Book you could have a callback that receives book and shelf:
const Book = ({ book, doSomethingWithBookAndShelf }) => {
return (
<div className="book">
<img
src={book.imageLinks.thumbnail}
alt={book.title}
className="book-thumbnail"
/>
<ButtonDropDown
choices={['Currently Reading', 'Want to Read', 'Read', 'None']}
onSelectChoice={(choice) => {
// book came from the component props
doSomethingWithBookAndShelf(book, choice);
}}
/>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
);
};
And in ButtonDropDown:
const ButtonDropDown = ({ choices, label, onSelectChoice }) => {
const [active, setActive] = useState(false);
const toggleClass = () => {
setActive(!active);
};
return (
<div className="dropdown">
<button
type="button"
className="dropbtn"
onFocus={toggleClass}
onBlur={toggleClass}
>
<BsFillCaretDownFill />
</button>
<div
id="myDropdown"
className={`dropdown-content ${active ? `show` : `hide`}`}
>
<div className="dropdown-label">{label}</div>
{choices.map((choice, index) => (
<button
// eslint-disable-next-line react/no-array-index-key
key={index}
className="dropdown-choice"
onClick={() => { // we create an specific callback for each item
onSelectChoice(choice);
}}
type="button"
value={choice}
>
{choice}
</button>
))}
</div>
</div>
);
};
Hope that puts you in some direction.
Also, beware it is more React-like to work like that. Avoid using the event objects to get values (i.e. e.target.value).
I want to add to Chip an startIcon={<Icon />}
when click on a Chip.
The state of the icon is managed by chipsState.
In this code,
the state of all chips would change.
How can I change only the chipsState of the element that is clicked?
In this code, the state of all chips will change.
How can I change only the chipsState of the element that is clicked?
const Modal:React.FC<Props>= (props) => {
const {modalData} = props;
const [chipsState, setChipsState] = useState(false);
const onChipClick = (element:any) => {
setChipsState(chipsState => !chipsState);
}
return (
<div>
{
modalData.symtoms.map((element:any, index:number) => (
<div key={index}>
<Chip onClick={() => onChipClick(element)} startIcon={chipsState && <Icon />}>{element.description}</Chip>
</div>
))}
</div>
);
}
export default Modal;
To handle local state (and better testing), you should create a new custom Chip component with dedicated chipState.
interface CustomChipProps {
description: string
}
const CustomChip = (props: CustomChipProps) => {
const [chipState, setChipState] = useState(false);
return <Chip onClick={() => setChipState(prev => !prev)} startIcon={chipState && <Icon />}>{props.description}</Chip>;
}
const Modal:React.FC<Props>= (props) => {
const {modalData} = props;
return (
<div>
{
modalData.symtoms.map((element:any, index:number) => (
<div key={index}>
<CustomChip description={element.description} />
</div>
))}
</div>
);
}
export default Modal;
You can achieve your desired output by changing chipState state from boolean to object.
So first let's change to object state instead of boolean
const [chipsState, setChipsState] = useState({});
Now we will change onChipClick function to change value of selected chip state
const onChipClick = (element:any) => {
setChipsState({...chipsState, chipsState[element]: !chipsState[element]});
}
And finally we will read correct value of each chipsState element.
<Chip onClick={() => onChipClick(element)} startIcon={chipsState[element] && <Icon />}>{element.description}</Chip>
You can try like the following
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import { Grid, Row } from "react-flexbox-grid";
const ChipSet = ({ symtomsData }) => {
const data = symtomsData.map((symtom) => ({ ...symtom, isSelcted: false }));
const [chipSets, setChipSets] = useState(data);
const onSelectChipSet = useCallback(
(e, index) => {
const updatedChipSets = chipSets.map((chip, i) =>
i === index ? { ...chip, isSelcted: e.target.checked } : chip
);
setChipSets(updatedChipSets);
},
[chipSets]
);
console.log("chipSets", chipSets);
return (
<div>
<h1>Symtoms Data</h1>
{chipSets.map((x, i) => (
<div key={i}>
<label>
<input
onChange={(e) => onSelectChipSet(e, i)}
type="checkbox"
value={x.isSelcted}
/>
{x.description}
</label>
</div>
))}
</div>
);
};
class App extends React.Component {
render() {
const symtomsData = [
{
description: "mild"
},
{
description: "cold"
}
];
return (
<Grid>
<Row>
<ChipSet symtomsData={symtomsData} />
</Row>
</Grid>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));