I'm trying to use React Hooks to update the list of items. When users click on the check-box, the app should render the items which have been selected as purchased.
I could log the event of the handPurchase() and the state is correct, however, I can't make the function render the latest state.
With class, I can do:
const handlePurchase() {
// ...
this.setState(updatedGroceries);
}
this.state.groceries.map(//...render list
This is the code:
export default function CheckboxList() {
let initialGroceries = [
{
id: "one",
text: "Apple",
purchased: false
},
{
id: "two",
text: "Banana",
purchased: false
}
];
const [groceries, setGroceries] = useState(initialGroceries);
const handlePurchase = (id: string) => () => {
groceries.forEach((item) => {
if (item.id === id) {
item.purchased = true;
}
});
setGroceries(groceries);
}
return (
<List>
{groceries.map((item) => {
const labelId = `checkbox-list-label-${item.id}`;
return (
<ListItem key={item.id} role={undefined} dense button onClick={handlePurchase(item.id)}>
<ListItemIcon>
<Checkbox
checked={item.purchased}
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={item.text} />
</ListItem>
);
})}
</List>
);
}
Also, if I do this:
const updatedGroceries = groceries.forEach((item) => {
if (item.id === id) {
item.purchased = true;
}
});
setGroceries(updatedGroceries);
I get this error:
Argument of type 'void' is not assignable to parameter of type 'SetStateAction<{ id: string; text: string; purchased: boolean; }[]>'. TS2345
The issue is that forEach does not return anything as you might expect. I guess you would like to update with setGroceries the purchased property on the elements where the provided id is equals to the current one to true. So you need to return an array for example with Array.prototype.map(). From the documentation:
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
I guess the following can work for you:
const updatedGroceries = groceries.map((item) => {
if (item.id === id) {
item.purchased = true;
}
return item;
});
setGroceries(updatedGroceries);
In your code you were assigning to updatedGroceries the forEach result which is undefined because it does not return anything, find here in the docs.
I hope that helps!
Related
I am trying to dynamically add and remove text fields for user entry.
When I click the button to remove a particular row, I want to modify the state object to remove the data of that row thereby causing the row to disappear. However I am unable to do that and I am getting an error in the render loop as the compiler is unable to find the value for the row.
The error is as follows
Cannot read properties of undefined (reading 'from')
I want it to look at the new state object and display the number of rows accordingly.
Here is the code for sandbox
import "./styles.css";
import React from "react";
import { Button, Grid, Paper } from "#mui/material";
import { TextField, Icon } from "#mui/material";
interface State {
serialInputObjects: any;
}
class SerialQRScanClass extends React.PureComponent<State> {
state = {
serialInputObjects: {
0: { from: "", to: "", except: "" }
}
};
//Delete the already registered scanned codes code here
handleAdd = () => {
const objectLength = Object.keys(this.state.serialInputObjects).length;
console.log(objectLength);
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[objectLength]: {
from: "",
to: "",
except: "",
fromError: "",
toError: ""
}
}
}));
console.log(this.state.serialInputObjects);
};
handleChangeFromSerials = (key: any, data: string) => {
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[key]: { ...prevState.serialInputObjects[key], from: data }
}
}));
console.log(this.state.serialInputObjects);
//this.calculation(key);
};
handleChangeToSerials = (key: any, data: string) => {
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[key]: { ...prevState.serialInputObjects[key], to: data }
}
}));
console.log(this.state.serialInputObjects);
//this.calculation(key);
};
handleRemove = (key) => {
console.log(this.state.serialInputObjects);
this.setState((prevState) => ({
...prevState,
serialInputObjects: { ...prevState.serialInputObjects, [key]: undefined }
}));
console.log(this.state.serialInputObjects);
};
render() {
return (
<Paper elevation={3} className="abc">
<Button onClick={this.handleAdd}>ADD NEW FIELD</Button>
{Object.keys(this.state.serialInputObjects).map((key) => (
<div key={key}>
<Grid container alignItems="flex-end">
<Grid item className="bcd">
<TextField
fullWidth
label={"FROM"}
placeholder={"Ex.100"}
value={this.state.serialInputObjects[key]["from"]}
onChange={(e) =>
this.handleChangeFromSerials(key, e.target.value)
}
error={
Boolean(this.state.serialInputObjects[key]["fromError"]) ||
false
}
helperText={this.state.serialInputObjects[key]["fromError"]}
margin="none"
size="small"
/>
</Grid>
<Grid item className="bcd">
<TextField
fullWidth
label={"To"}
placeholder={"Ex.100"}
value={this.state.serialInputObjects[key]["to"]}
onChange={(e) =>
this.handleChangeToSerials(key, e.target.value)
}
error={
Boolean(this.state.serialInputObjects[key]["toError"]) ||
false
}
helperText={this.state.serialInputObjects[key]["toError"]}
margin="none"
size="small"
/>
</Grid>
<Grid
item
className={"abc"}
style={{ paddingLeft: "10px" }}
></Grid>
<div style={{ display: key === "0" ? "none" : "block" }}>
<Button onClick={(e) => this.handleRemove(key)}>
<Icon fontSize="small">remove_circle</Icon>
</Button>
</div>
</Grid>
</div>
))}
</Paper>
);
}
}
export default function App() {
return (
<div className="App">
<SerialQRScanClass />
</div>
);
}
I want to modify the state object to remove the data of that row thereby causing the row to disappear.
If you want to cause the row to disappear you have to update the serialInputObjects object without the row you want to delete.
Right now you are just assigning the value undefined to the selected row so it still exists but it doesn't contain the property from anymore, and because you are referring to that property here:
value={this.state.serialInputObjects[key]["from"]}
Javascript tells you that it is undefined, beacuse it doesn't exists.
Now for this you will need destructuring assigment, this is the solution:
handleRemoveKey(key){
const { [key]: renamedKey, ...remainingRows } = this.state.serialInputObjects;
this.setState((prevState) => ({
...prevState,
serialInputObjects: { ...remainingRows }
}));
}
However if you want to know why that first line of the function works, follow me.
Let's say you have this object:
const myObj = {
a: '1',
b: '2',
c: '3',
}
And let's say we need to separate the c prop from the others, we do that like this:
const { c, ...otherProps } = myObj
This would result in the creation of 2 new const, the const c and the const otherProps:
The const c will contain the value '3'
The const otherProps will contain this object { a: '1', b: '2' }
But what happen if there is already a variable named c? Our newly created c const in the destructuring statement would be a duplicate which is not allowed of course, so what can we do? We rename our newly created c const while we are destructuring myObj, like this:
const { c: renamedC, ...otherProps } = obj
This way the newly created const would be renamedC and therefore there will be no conflict with the other c we just supposed for the example.
That is exactly we are doing here:
handleRemoveKey(key){ // <------ here is already a "variable" named key
const { [key]: renamedKey, ...remainingRows } = this.state.serialInputObjects;
// so we had to rename our newly created 'key' const to 'renamedKey' to avoid conflicts.
As a side note I would suggest serialInputObjects should be an array(of objects) and not an object because arrays are ordered while objects are not, this whole process would have been easier if serialInputObjects would have be an array.
I would like to delete selected item from list.
When I click on delete the right item get deleted from the list content but on UI I get always the list item fired.
I seems to keep track of JSX keys and show last values.
Here's a demo
const Holidays = (props) => {
console.log(props);
const [state, setState] = useState({ ...props });
useEffect(() => {
setState(props);
console.log(state);
}, []);
const addNewHoliday = () => {
const obj = { start: "12/12", end: "12/13" };
setState(update(state, { daysOffList: { $push: [obj] } }));
};
const deleteHoliday = (i) => {
const objects = state.daysOffList.filter((elm, index) => index != i);
console.log({ objects });
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
};
return (
<>
<Header as="h1" content="Select Holidays" />
<Button
primary
icon={<AddIcon />}
text
content="Add new holidays"
onClick={() => addNewHoliday(state)}
/>
{state?.daysOffList?.map((elm, i) => {
console.log(elm.end);
return (
<Flex key={i.toString()} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
key={i.toString()}
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>
<span>{JSON.stringify(state.daysOffList)}</span>
</Flex>
);
})}
</>
);
};
export default Holidays;
Update
I'm trying to make a uniq id by adding timeStamp.
return (
<Flex key={`${JSON.stringify(elm)} ${Date.now()}`} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
primary
key={`${JSON.stringify(elm)} ${Date.now()}`}
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>{" "}
</Flex>
);
I was hoping that the error disappear but still getting same behaviour
Issue
You are using the array index as the React key and you are mutating the underlying data array. When you click the second entry to delete it, the third element shifts forward to fill the gap and is now assigned the React key for the element just removed. React uses the key to help in reconciliation, if the key remains stable React bails on rerendering the UI.
You also can't console log state immediately after an enqueued state update and expect to see the updated state.
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
React state updates are asynchronous and processed between render cycles. The above can, and will, only ever log the state value from the current render cycle, not the update enqueued for the next render cycle.
Solution
Use a GUID for each start/end data object. uuid is a fantastic package for this and has really good uniqueness guarantees and is incredibly simple to use.
import { v4 as uuidV4 } from 'uuid';
// generate unique id
uuidV4();
To specifically address the issues in your code:
Add id properties to your data
const daysOffList = [
{ id: uuidV4(), start: "12/12", end: "12/15" },
{ id: uuidV4(), start: "12/12", end: "12/17" },
{ id: uuidV4(), start: "12/12", end: "12/19" }
];
...
const addNewHoliday = () => {
const obj = {
id: uuidV4(),
start: "12/12",
end: "12/13",
};
setState(update(state, { daysOffList: { $push: [obj] } }));
};
Update handler to consume id to delete
const deleteHoliday = (id) => {
const objects = state.daysOffList.filter((elm) => elm.id !== id);
setState(update(state, { daysOffList: { $set: objects } }));
};
Use the element id property as the React key
{state.daysOffList?.map((elm, i) => {
return (
<Flex key={elm.id} gap="gap.small">
...
</Flex>
);
})}
Pass the element id to the delete handler
<Button
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(elm.id)}
/>
Use an useEffect React hook to log any state update
useEffect(() => {
console.log(state.daysOffList);
}, [state.daysOffList]);
Demo
Note: If you don't want (or can't) install additional 3rd-party dependencies then you can roll your own id generator. This will work in a pinch but you should really go for a real proven solution.
const genId = ((seed = 0) => () => seed++)();
genId(); // 0
genId(); // 1
I want to remove object from my list by clicking on delete icon, but with my logic either everything is deleted from list or nothing, I am not sure how to do it without provided ID to object, I don't have anything unique and I am kinda lost.
Component that renders as many Food as there is in useState:
{cartFood.map((food) => {
return (
<CartFood
key={Math.random()}
foodName={food.foodName}
foodPrice={food.foodPrice}
numberOfPortions={food.numberOfPortions}
cartFood={cartFood}
setCartFood={setCartFood}
/>
);
})}
Logic for removing that particular item that is selected (which is not working and also bad solution since there can be case where you get same name and price twice)
const CartFood = ({
foodName,
foodPrice,
numberOfPortions,
cartFood,
setCartFood,
}) => {
const handleRemoveFood = () => {
setCartFood(
cartFood.filter(
(el) =>
el.foodName &&
el.foodPrice !== cartFood.foodName &&
cartFood.foodPrice
)
);
};
return (
<div className='cartFood-container'>
<p>{foodName}</p>
<p>x{numberOfPortions}</p>
<p>{foodPrice}kn</p>
<p>
<MdDeleteForever
className='cartFood__icon'
onClick={handleRemoveFood}
/>
</p>
</div>
);
};
export default CartFood;
List of objects looks like this:
[{
foodName: "Njoki with sos"
foodPrice: 35
numberOfPortions: 1
},
{
foodName: "Chicken Wingos"
foodPrice: 45
numberOfPortions: 2
}]
Put the index of the item in the array as the id. Pass it as your id.
{cartFood.map((food, index) => {
return (
<CartFood
key={index}
id={index}
foodName={food.foodName}
foodPrice={food.foodPrice}
numberOfPortions={food.numberOfPortions}
cartFood={cartFood}
setCartFood={setCartFood}
/>
);
})}
Use the id to remove the food.
const CartFood = ({
foodName,
foodPrice,
numberOfPortions,
cartFood,
setCartFood,
id,
}) => {
const handleRemoveFood = () => {
setCartFood(cartFood.filter((el) => el.id !== id));
};
return (
<div className='cartFood-container'>
<p>{foodName}</p>
<p>x{numberOfPortions}</p>
<p>{foodPrice}kn</p>
<p>
<MdDeleteForever
className='cartFood__icon'
onClick={handleRemoveFood}
/>
</p>
</div>
);
};
Something like this should work :
const handleRemoveFood = (obj) => {
setCartFood((oldList) => oldList.filter((item) => item.foodName !== obj.foodName));
};
Your button (icon) should call this function with current object data (obj)
A working example : https://codesandbox.io/s/cart-isz6c?file=/index.js
From what I see in your repo:
Just pass the food._id to FoodCard so you access it when you want to add or remove an item from cart:
FoodList.js
const foodList = (typeOfList) =>
typeOfList.map(food => {
return (
<FoodCard
key={food._id}
foodId={food._id}
foodName={food.title}
foodPrice={food.price}
foodPic={food.image}
setCartFood={setCartFood}
cartFood={cartFood}
/>
);
});
FoodCard.js
const handleAddToCard = () => {
setCartFood([
...cartFood,
{
foodId,
foodName,
foodPrice,
numberOfPortions,
},
]);
};
CartFood.js
const handleRemoveFood = () => {
setCartFood(cartFood => cartFood.filter((el) => el.foodId !== foodId));
};
Working example:
You could use useReducer with useContext so you don't have to pass props down manually at every level, check this article for more info
You don't need to pass the cartFood as a property just for updating the state since you can use setState callback:
setCartFood(cartFood => [
...cartFood,
{
foodId,
foodName,
foodPrice,
numberOfPortions,
},
]);
My objective is to toggle switch off and on as per the respective id, but i couldn't able to toggle the switch.
i have tried in this way:
<Switch
checked={data.isShow}
onChange={this.handleChange}
color="primary"
name={data.isShow ? "On" : "Off"}
inputProps={{ "aria-label": "primary checkbox" }}
/>
And OnChange i have written in this way:
handleChange = () => {
this.setState({ isShow: !this.state.isShow });
};
Here is the sample
Can anyone help me in this query?
You should handleChange for specific element. Here I pass the id of the element, and toggle isShow of that element only
handleChange = (id) => {
this.setState({
info: this.state.info.map((i) => {
if (i.id === id) {
return {
...i,
isShow: !i.isShow
};
} else {
return i;
}
})
});
};
// ...
<Switch
checked={data.isShow}
onChange={() => this.handleChange(data.id)}
color="primary"
name={data.isShow ? "On" : "Off"}
inputProps={{ "aria-label": "primary checkbox" }}
/>
Forked demo
No need of state variable just update info array in handleChange(). Below are the changes in handleChange() method
handleChange = (singleRowData) => {
console.log(singleRowData);
const {info} = this.state;
const index = info.findIndex(data => data.id === singleRowData.id);
info[index].isShow = !info[index].isShow;
this.setState({ info: [...info] });
};
Change handleChange function to:
handleChange = (event) => {
let changeId = parseInt(event.target.id);
let info = this.state.info.map((data) => {
if (data.id === changeId) {
return Object.assign({}, data, {
isShow: !data.isShow
});
}
return data;
});
this.setState({info: info});
};
Include id as part of the Component params
<Switch
checked={data.isShow}
onChange={this.handleChange}
id={data.id}
color="primary"
name={data.isShow ? "On" : "Off"}
inputProps={{ "aria-label": "primary checkbox" }}
/>
Edit:
Made edits to the sample link you've provided - it's working here
You need to change the isShow property from the item in the info array (in your state).
you can use find() in order to find the item with the same id,
but before that you need to use slice() to copy info array, because you don't mutate your state.
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
(Referenced from React docs)
your handleChange() method should look like this then:
handleChange = (id) => {
const data = this.state.info.slice();
const item = data.find((item) => item.id === id);
item.isShow = !item.isShow;
this.setState(data);
};
Here is the full result:
https://codesandbox.io/s/async-bird-998ue?fontsize=14&hidenavigation=1&theme=dark
Is there a way to handle the checked state of an array of checkboxes?
I have this array:
const CheckboxItems = t => [
{
checked: true,
value: 'itemsCancelled',
id: 'checkBoxItemsCancelled',
labelText: t('cancellations.checkBoxItemsCancelled'),
},
{
checked: true,
value: 'requestDate',
id: 'checkboxRequestDate',
labelText: t('cancellations.checkboxRequestDate'),
},
{
checked: true,
value: 'status',
id: 'checkboxStatus',
labelText: t('cancellations.checkboxStatus'),
},
{
checked: true,
value: 'requestedBy',
id: 'checkboxRequestedBy',
labelText: t('cancellations.checkboxRequestedBy'),
},
];
And I am using it here:
class TableToolbarComp extends React.Component {
state = {
isChecked: true,
};
onChange = (value, id, event) => {
this.setState(({ isChecked }) => ({ isChecked: !isChecked }));
};
render() {
const { isChecked } = this.state;
return (
{CheckboxItems(t).map(item => (
<ToolbarOption key={item.id}>
<Checkbox
id={item.id}
labelText={item.labelText}
value={item.value}
checked={isChecked}
onChange={this.onChange}
/>
</ToolbarOption>
))}
)
}
}
The problem I am having is that every time I unchecked one, the rest of them get unchecked too. I need to manage the state separately to send some information to other components through a redux action.
EDIT:
This is the UI library I am using
You're using the container's isChecked as the state for all of your checkboxes, using a method on your container to flip that one flag that it applies to all of them (isChecked).
Instead, either:
Give the checkboxes themselves state, rather than making them simple objects, or
Maintain a state map in the container keyed by the checkbox item (or perhaps its name)
I would lean toward #1, which I think would look like this with that library:
class TableToolbarComp extends React.Component {
state = {
items: CheckboxItems(t) // Your code seems to have a global called `t`
};
onChange = (value, id, event) => {
this.setState(({ items }) => {
// Copy the array
items = items.slice();
// Find the matching item
const item = items.find(i => i.id === id);
if (item) {
// Update its flag and set state
item.checked = !item.checked;
return { items };
}
});
};
render() {
const { items } = this.state;
return (
{items.map(item => (
<ToolbarOption key={item.id}>
<Checkbox
id={item.id}
labelText={item.labelText}
value={item.value}
checked={item.checked}
onChange={this.onChange}
/>
</ToolbarOption>
))}
)
}
}
Changes:
Call CheckboxItems once, keep the result as state.
In onChange, find the relevant checkbox by id (the lib passes the id) and flip its checked flag
In render, get the items from state and for each item, use its checked flag, not your `isChecked (which I've removed entirely