Deleting list Item in react keep older items instead - javascript

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

Related

React state updating without setstate, takes on state of deleted item (SOLVED)

I have a React notes app that has a delete button, and a state for user confirmation of deletion.
Once user confirms, the 'isConfirmed' state is updated to true and deletes the item from MongoAtlas and removes from notes array in App.jsx.
The problem is, the note that takes the index (through notes.map() in app.jsx I'm assuming) of the deleted notes position in the array has the 'isConfirmed' state set to true without calling setState. Thus, bugging out my delete button to not work for that specific note until page refresh.
I've included relevant code from my Delete Component:
function DeletePopup(props) {
const mountedRef = useRef(); //used to stop useEffect call on first render
const [isConfirmed, setIsConfirmed] = useState(false);
const [show, setShow] = useState(false);
function confirmDelete() {
// console.log("user clicked confirm");
setIsConfirmed(true);
// console.log(isConfirmed);
handleClose();
}
useEffect(() => {
// console.log("delete useEffect() run");
if (mountedRef.current) {
props.deleteNote(isConfirmed);
}
mountedRef.current = true;
}, [isConfirmed]);
Note Component:
function Note(props) {
function deleteNote(isConfirmed) {
props.deleteNote(props.id, { title: props.title, content: props.content }, isConfirmed);
console.log("note.deleteNote ran with confirmation boolean: " + isConfirmed);
}
return <Draggable
disabled={dragDisabled}
onStop={finishDrag}
defaultPosition={{ x: props.xPos, y: props.yPos }}
>
<div className='note'>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleClick}>
{dragDisabled ? <LockIcon /> : <LockOpenIcon />}
</button>
<EditPopup title={props.title} content={props.content} editNote={editNote} />
<DeletePopup deleteNote={deleteNote} />
</div>
</Draggable>
}
App Component:
function App() {
const [notes, setNotes] = useState([]);
function deleteNote(id, deleteNote, isConfirmed) {
if (!isConfirmed) return;
axios.post("/api/note/delete", deleteNote)
.then((res) => setNotes(() => {
return notes.filter((note, index) => {
return id !== index;
});
}))
.catch((err) => console.log(err));
}
return (
<div id="bootstrap-override">
<Header />
<CreateArea
AddNote={AddNote}
/>
{notes.map((note, index) => {
return <Note
key={index}
id={index}
title={note.title}
content={note.content}
xPos={note.xPos}
yPos={note.yPos}
deleteNote={deleteNote}
editNote={editNote}
/>
})}
<Footer />
</div>);
}
I've tried inserting log statements everywhere and can't figure out why this is happening.
I appreciate any help, Thanks!
EDIT: I changed my Notes component to use ID based on MongoAtlas Object ID and that fixed the issue. Thanks for the help!
This is because you are using the index as key.
Because of that when you delete an element you call the Array.filter then you the elements can change the index of the array which when React tries to rerender the notes and as the index changes it cannot identify the note you've deleted.
Try using a unique id (e.g. an id from the database or UUID) as a key instead.
I hope it solves your problem!

Passing an object Id by submit button in order to find and change value in array after submiting (react)

I want to update a value of an object nested in array after choosing new date from select button and then submiting it in order to change it.
The button (select) is nested with a rendered array object invoice in Accordion from material ui and is supposed to change a date (for now a year only) and save it while comparing its Id number.
I have two components : sideBar and simpleForm
const SideBar = ({ className }) => {
const [invoices, setInvoice] = useState([
{ label: 'Test', invoiceDate: '2021', id: 0 },
{ label: 'Test', invoiceDate: '2022', id: 1 },
{ label: 'Test', invoiceDate: '', id: 2 },
])
const addInvoiceDate = (date, invoice) => {
setInvoice(
invoices.map((x) => {
if (x.id === invoice.id)
return {
...x,
invoiceDate: date,
}
return x
})
)
}
return (
<>
<Wrapper>
<S.MainComponent>
<div>
{invoices.map((invoice) => {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<div key={invoice.id}>
{invoice.label}
{invoice.invoiceDate}
</div>
</AccordionSummary>
<AccordionDetails>
<SimpleForm addInvoiceDate={addInvoiceDate} />
</AccordionDetails>
</Accordion>
)
})}
</div>
</S.MainComponent>
</Wrapper>
</>
)
}
Simple Form :
const SimpleForm = ({ addInvoiceDate }) => {
const [date, setDate] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
addInvoiceDate(date)
}
function range(start, end) {
return Array(end - start + 1)
.fill()
.map((_, placeholder) => start + placeholder)
}
const Years = range(2021, 4000)
const Options = []
Years.forEach(function (element) {
Options.push({ label: element, value: element })
})
return (
<form onSubmit={handleSubmit}>
<select value={date} required onChange={(e) => setDate(e.target.value)}>
{Options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
<input type='submit' value='Save' />
</form>
)
}
My problem is, i have no clue how can i succesfully pass an id number of array object to addInvoiceDate (change invoice date)in order to find it in orginal array, compare it and then submit new value. I was testing adding a new object with a new year value and it worked, but in that case i dont have to find an id of object. Its a little bit harder if i want actually find a previous one and update it through comparing id.
Any ideas how it should be done ? Probably i overlooked something or dont have enough expierience yet :)
How about this
<SimpleForm addInvoiceDate={(date) => addInvoiceDate(date, invoice)} date={invoice.invoiceDate}/> in SideBar's return
Remove the state from SimpleForm as we now have a date prop instead
<select value={date} required onChange={(e) => addInvoiceDate(e.target.value)}> in SimpleForm's return
Please leave a comment if there are questions

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

React remove object from list of objects without unique value, using hooks

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,
},
]);

Restructure repetitive component to get rendered within a map function

While I wrote my component as much DRY as I could, I wonder if there is a better way to render ConnectButton through a map function that takes [linkedIn, twitter, researchGate] as an array. I tried different things, but that's as good as I could get it. (I simplified the code a bit to the relevant. The original version is by a lot longer.)
const Component = () => {
const ConnectButton = ({ icon, title }) => (
<MenuItem>
<ListItemIcon className={classes.listItemIcon}>
<Icon className={classes.connectIcon} icon={icon} />
</ListItemIcon>
<ListItemText primary={title} />
</MenuItem>
);
const renderConnectMenu = () => {
if (!profileInfo) {
return;
}
const linkedIn = profileInfo.linkedIn;
const twitter = profileInfo.twitter;
const researchGate = profileInfo.researchGate;
return (
<div>
{linkedIn && <ConnectButton icon={faLinkedin} title="LinkedIn" />}
{twitter && <ConnectButton icon={faTwitter} title="Twitter" />}
{researchGate && (
<ConnectButton icon={faResearchgate} title="Research Gate" />
)}
</div>
);
};
render(
<>
[...]
{renderConnectMenu()}
[...]
</>
);
};
There are 3 properties that define the rendering of a button: a field from profileInfo, icon and title. So we will need an array of objects with these properties in order to render them with .map.
I am not sure what kind of date the profileInfo fields hold so I named the property as shouldRender based on its usage.
const renderConnectMenu = () => {
if (!profileInfo) {
return;
}
const buttons = [
{
shouldRender: profileInfo.linkedIn,
icon: faLinkedin,
title: 'LinkedIn',
},
{
shouldRender: profileInfo.twitter,
icon: faLinkedin,
title: 'Twitter',
},
{
shouldRender: profileInfo.researchGate,
icon: faLinkedin,
title: 'Research Gate',
},
];
return (
<div>
{buttons.map(
(button) =>
button.shouldRender && (
<ConnectButton
key={button.title}
icon={button.icon}
title={button.title}
/>
),
)}
</div>
);
};
It might look a bit weird when you have a few items to render. So I wouldn't recommend this approach if there are just 2-3 items unless there is a possibility of extending the list. It's okay to keep it as it is now.

Categories