How to remove nested object in react state without affecting loop - javascript

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.

Related

Why is my number coming up as an object when passed in the prop?

I have a component FooterScroll. And the main page is TvIndex.
This is the FooterScroll Component
const FooterScroll = (Id: number) => {
const { event } = useEvent(Id);
console.log('Id', Id);
return (
<Marquee speed={85}>
<Grid container>
{event?.notices.map((notice: EventNotice) => {
if (notice.approved) {
return (
<>
<Grid item px={4} key={notice.id}>
<CampaignIcon />
</Grid>
<Grid item alignItems="center">
{notice.body}
</Grid>
</>
);
}
})}
</Grid>
</Marquee>
);
};
export default FooterScroll;
This is the TvIndex Page
import FooterScroll from 'components/pages/members/tv/scrolling-footer';
const TvIndex: React.FC = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<div className={classes.scrollFooter}>
<FooterScroll Id={39324} />
</div>
</div>
);
};
export default TvIndex;
For some reasons that I don't understand, when I pass in this any id number as a prop in my main component for FooterScroll, it gives me this error: Type '{Id: number;}' is not assignable to type 'number'.
Then I decided to console log it inside FooterScroll, and it looks like after the id gets passed in TvIndex, it passes inside the FooterScroll component as an Object, like so: Id > Object { Id: 39324 }.
How can I solve this?
The argument passed to a React component is an object, whose properties are the props passed down by the caller. For example
<FooterScroll Id={39324} />
results in a prop object of:
{
Id: 39324
}
React doing it this way allows for easily adding additional props, or properties to the props object, without changing the function signature much:
<FooterScroll Id={39324} someOtherProp="foo" />
results in a prop object of:
{
Id: 39324,
someOtherProp: 'foo'
}
So, you need to change the function definition to
const FooterScroll = ({ Id }: { Id: number }) => {
or do
const FooterScroll = (props: { Id: number }) => {
const { Id } = props;
Your arguments to the FooterScroll are wrong. Try this:
const FooterScroll = ({ Id }: { Id: number }) => {
...
}
It should work.

How do I add an list element seperately to 2 lists?

I am pretty new to react. So I have one parent component which has two child components. These 2 children are the lists that should be displayed. So far I figured out how to transfer the data between two lists by checking the status property of the data. I am not able to understand how to add data into the separate lists and edit them since the parent component renders the 2 lists. Can anyone explain how to add and edit new data that the user will enter? Should I create new states and props on the Items page or should I create them on the child component page? I am pretty confused.
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
<form onSubmit={this.addItem}>
<input placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p> </>
})
}
</div>
)
}
I have attached the 3 components which are Items, Pending and Completed above.
It's almost always better to have the state in the parent and pass down props to the children. So you want to keep your items state where it is. You can create an addItem function and pass it down as a prop to any child.
I don't think it makes sense to be able to add items from both lists since new items should be 'Pending'. So I would recommend that you put your add form in a new component AddItem which would be a third child of Items. Once AddItem calls the addItem function from props, that item will get saved to the state in items and it will show up in the Pending list automatically.
If all new items have status 'Pending' then the only information that we should need to add an item is the title of the task.
This function goes in Items:
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
Your AddItem component uses a controlled input to create the text for the new item.
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
Inside the return of Items, include your form:
<AddItem addItem={addItem} />
Unrelated to the question at hand, there are a few other improvements that you can make to your code.
Your updateStatus function actually mutates the current item. You should instead create a new object for the changed item by copying everything except the status.
You are getting warnings about unique keys because the key must be on the outermost component inside the .map(). You put a fragment <> outside the <p> which has the key, so remove the fragment.
In my opinion the filtering of which item goes in each list should be done by the parent. Your Completed and Pending components are extremely similar. You should combine them into one component. Everything that is different between the two, such as texts and class names, can be controlled by the props that you pass in.
import React, { useState } from "react";
export const ItemsList = ({
items,
title,
className,
buttonText,
onClickButton
}) => {
return (
<div className={className}>
<h1>{title}</h1>
{items.map((item) => (
<p className="item" key={item.id}>
<span className="item_title">{item.title}</span>
<button
className="move_item"
key={item.id}
onClick={() => {
onClickButton(item.id);
}}
>
{buttonText}
</button>
</p>
))}
</div>
);
};
// example of how to compose components
// this keeps the same setup that you had before, but without repeated code
export const Completed = ({ items, updateStatus }) => {
return (
<ItemsList
title="RIGHT"
buttonText="Move Left"
className="completed"
items={items.filter((item) => item.status === "Completed")}
onClickButton={(id) => updateStatus(id, "Pending")}
/>
);
};
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
export const Items = () => {
const [items, setItems] = useState([
{
id: 1,
title: "Workout",
status: "Pending"
},
{
id: 2,
title: "Read Books",
status: "Pending"
},
{
id: 3,
title: "Cook Pizza",
status: "Pending"
},
{
id: 4,
title: "Pay Bills",
status: "Completed"
},
{
id: 5,
title: " Watch Big Short",
status: "Completed"
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending"
}
]);
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
const updateStatus = (id, newStatus) => {
setItems((current) =>
// arrow function without braces is an implicit return
current.map((item) =>
item.id === id
? // copy to new item if id matches
{
...item,
status: newStatus
}
: // otherwise return the existing item
item
)
);
};
return (
<div className="items">
<AddItem addItem={addItem} />
{/* can set the props on ItemsList here */}
<ItemsList
title="LEFT"
buttonText="Move Right"
className="pending"
items={items.filter((item) => item.status === "Pending")}
// create a function that just takes the `id` and sets the status to "Completed"
onClickButton={(id) => updateStatus(id, "Completed")}
/>
{/* or do it in a separate component */}
<Completed items={items} updateStatus={updateStatus} />
</div>
);
};
export default Items;
Code Sandbox Link

Deleting list Item in react keep older items instead

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

Array not empty but length is 0 and JSON.stringify returns an empty list [duplicate]

This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
console.log() async or sync?
(3 answers)
Closed 2 years ago.
I have been working on an event signup page for a school club I am in, and I cannot seem to have my program read the contents of my array, and put each one in a ListItem, which goes in a List for the life of me.
When I call this function, the boolean expression this.state.events.length !== 0 evaluates to False, however console.log(this.state.events) shows the array is not empty.
What might be causing this and how do you propose I go about fixing this.
Side note: I would love some criticism of my code. I just started with JS, React, and MaterialUI and would like to improve.
class EventSelector extends Component {
constructor(props){
super(props);
this.state = {
events: []
}
}
componentDidMount = () => {
var lst = [] // this is a list of events s.t. {key: "event_name"}
const eventsRef = db
.collection('events'); // this is a reference to the events collection
const offeredRef = eventsRef
.where('allowRegistration', '==', true)
.get() // this contains all docs which allow registration.
.then((querySnapshot) => {
querySnapshot.forEach((doc) => { // for each document, create key value pairs
lst.push({key: doc.data().name, id: doc.id})
})
})
// console.log(lst)
this.setState({ events: lst })
return true;
}
EventList = (events) => {
const { classes } = this.props;
return(
<div>
<Grid container item
direction='column'
alignItems='center'
justify='center'
style={{ maxHeight: '70vh', maxWidth: '50vw' }}
spacing={5}>
<Grid item>
<h1 className={classes.h1}>Upcoming Events</h1>
<h2 className={classes.h2}>Please select an event to sign up</h2>
</Grid>
<Grid container item direction='row' justify='center' spacing={5}>
<List component='b' subheader={<ListSubheader componenet='b'>Upcomming Events</ListSubheader>}>
{events.map(( {key , id} ) => {
// console.log(key)
return (
<div key={id}>
<ListItem button>
<ListItemText inset primary={key}/>
</ListItem>
</div>
);
}) }
</List>
</Grid>
</Grid>
</div>
);
}
// }
render = () => {
// const { classes, lists } = this.props;
const { classes } = this.props;
console.log(this.state.events)
var obj = Object.assign({}, this.state.events)
console.log(JSON.stringify(obj))
return (this.state.events.length !== 0 ? <h1>{JSON.stringify(this.state.events)}</h1> : <h2>Loading</h2>)
}
}
export default withStyles(styles)(EventSelector);
console output with Object.assign()
console output without Object.assign()
As far as I can see, there is nothing wrong with your code. One interesting thing about React, is that all your state changes will result into a new call to the render method.
state = { events: [] };
componentDidMount() {
// const lst = ...
this.setState({ events: lst });
// no need return here
};
render() {
console.log(this.state.events);
}
First time, it will print [] because your state is initialized this way. After the component is mounted, events will be print just like it was filled.
Another way to write this code is using a functional component:
import { useState } from 'react';
const EventSelector = () => {
const [events, setEvents] = useState([]);
useEffect(() => {
// const lst = ...
setEvents(lst);
}, []);
return events.length !== 0 ? (
<h1>{JSON.stringify(this.state.events)}</h1>
) : (
<h2>Loading</h2>
);
}
IMHO, functional components are better to read. This code works just as yours, the same behavior is expected.

Why my check box isn't updated after click?

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!

Categories