While trying to build a React Component that renders a list of objects I ran into some behaviour that I cannot explain.
I use the useState hook to keep track of a list of animals.
When I click the button 'add animal', I add an object to that list containing a random animal.
I do this several times.
...so far so good, an object is created and added every time, and my animalList is properly rendered.
But here is where I get lost;
When I click the 'Remove' button on an Animal component it logs the animalList to the console, but it shows a different value for every item in the list, while I expect it to be the same for all of them.
It appears that the value of animalList is equal to what it was at the time the object was created rather than referencing the state.
My next step would be to remove the clicked object, but they don't seem to share the same reference to the list.
Can somebody help me understand what is happening here? I have added the code required to recreate the issue:
import { useState } from 'react';
import './App.css';
export default function App() {
const [animalList, updateAnimalList] = useState([]);
const animals = [
{name: 'anaconda'}, {name: 'brachiosaurus'}, {name: 'chimpansee'}, {name: 'dragon'}, {name: 'eagle'}, {name: 'fox'},
{name: 'giraffe'}, {name: 'hellhound'}, {name: 'iguana'}, {name: 'jackal'}, {name: 'koala'}, {name: 'lion'},
{name: 'meerkat'}, {name: 'nyan-cat'}, {name: 'ostrich'}, {name: 'pterodactyl'}, {name: 'quail'}, {name: 'rhinoceros'},
{name: 'sfinx'}, {name: 'triceratops'}, {name: 'unicorn'}, {name: 'vampire deer'}, {name: 'whale'}, {name: 'xiao'},
{name: 'yoghurt'}, {name: 'zebra'},
];
const addAnimal = () => {
updateAnimalList([...animalList,
{ ...animals[Math.floor(Math.random() * animals.length)],
onClick: removeAnimal,
}
]);
}
const removeAnimal = () => {
console.log(animalList);
// let newArray = [...animalList];
//newArray.splice(index, 1);
//updateAnimalList(animalList);
};
return (
<div className="app">
<button onClick={addAnimal}>addAnimal</button>
{ animalList.map( (animal, index) => {
return (
<Animal key={index} {...animal} />
)
})}
</div>
);
};
export function Animal(animal) {
return (
<div className="card">
<h2>{ animal.name }</h2>
<button onClick={animal.onClick}>Remove</button>
</div>
)
}
I would avoid storing onClick in animalList because removeAnimal captures a stale reference to a version of animalList.
updateAnimalList can be used like you did or it can also accept an updater function which receives the current value of animalList.
So by combining these two, I would end up with something like this:
const animals = [
// the animals (should be defined outside of the component as it is not changing)
];
export default function App() {
const [animalList, updateAnimalList] = useState([]);
const addAnimal = () => {
updateAnimalList([
...animalList,
animals[Math.floor(Math.random() * animals.length)]
]);
}
const makeRemoveAnimal = (index) => () => {
updateAnimalList((current) => [...current].splice(index, 1))
};
return (
<div className="app">
<button onClick={addAnimal}>addAnimal</button>
{animalList.map((animalName, index) => {
return (
<Animal key={index} name={animalName} onClick={makeRemoveAnimal(index)} />
)
})}
</div>
);
};
Because every time you add an animal here:
{ ...animals[Math.floor(Math.random() * animals.length)],
onClick: removeAnimal,
}
you are also storing a function reference (removeAnimal) in the state.
The version of removeAnimal which you are storing is from the render when the click happened (aka stale closure). Hence, inside removeAnimal:
let newArray = [...animalList];
the animalList is also from the render when the click happened.
No reason to store removeAnimal on each click inside array. Just declare it as function and pass an id of object you want to delete. Then you can always use that single function.
Also you seem to be using index as key which is not recommended especially if array items may reorder. Use some id instead.
So you could do:
{
animalList.map((animal) => {
return (
<Animal
key={animal.id}
{...animal}
onClick={() => removeAnimal(animal.id)}
/>
);
});
}
Then
const removeAnimal = (id) => {
updateAnimalList(animalList.filter((x) => x.id != id));
};
It is not good to store link onClick in animalList because link becomes unactual.
It looks like you should send name of animal. When you will send name of animal, then it would be pretty simple to remove item of array:
// you need to send here name of animal
const removeAnimal = () => {
const updatedArray = animalList.filter( p=> p.name !== 'anaconda')
updateAnimalList(prevAnimals => [...updatedArray]);
};
You can get the same function with useRef
const [animalList, updateAnimalList] = useState([]);
const refRemoveFunction = useRef(() => {});
const addAnimal = () => {
updateAnimalList([
...animalList,
{
...animals[Math.floor(Math.random() * animals.length)],
onClick: refRemoveFunction
}
]);
};
useEffect(() => {
refRemoveFunction.current = (index) => {
animalList.splice(index, 1);
updateAnimalList([...animalList]);
};
}, [animalList]);
And you can use like this:
export function Animal(animal) {
return (
<div className="card">
<h2>{animal.name}</h2>
<button onClick={() => animal.onClick.current(animal.index)}>Remove</button>
</div>
);
}
I hope, It will be useful for you.
Related
I'm writing a program where you can add reviews to a list in React. I also added a feature to delete the reviews. Each review is a component stored in a State array. I wrote a function, removeItem, that updates the state by creating a duplicate of the array and popping the passed index". Each review is given a handleFeature property where this removeItem function is passed, and an id which corresponds to it's index in the array.
Inside the review component, it has an onclick event which calls the removeItem function through handleFeature, passing it's own id in the array. I thought this would cause the array to update and remove the item; however, It causes multiple items to get deleted for no apparent reason. Does anyone know the fix to this issue
Data
export default[
{
id: 0,
name: "Mindustry",
score: "4.5"
},
{
id: 1,
name: "Minecraft",
score: "4"
},
{
id: 2,
name: "Plants vs Zombies",
score: "4"
},
]
App
import './App.css';
import jSData from './components/jSData.js'
import Card from './components/Card.js'
import React from 'react';
function App() {
//we are mapping the review data to an array of cards using an lamba expression
//this is a state object. Once it changes, the webpage is updated
//it returns the object and a function to change it
//the object is immutable; however, you can reference it to make updates
const [reviews, changeState] = React.useState(jSData.map((item) => {
return (<Card
//key is necessary for list items
key = {item.id}
handleEvent = {removeItem}
//name = {item.name}
//score = {item.score}
//the above can be simplified to
{...item}
/>);
}
));
function submit(e)
{
//prevent reloading
e.preventDefault();
//spreading the original array + card into a new array
/*changeState(
[...reviews,
<Card
id = {reviews.length}
name = {document.getElementById('form_name').value}
score = {document.getElementById('form_score').value}
/>]
);*/
//best practice to use the higher order version of state change
//this should contain a function which returns the new state
changeState(oldValue =>
[...oldValue,
<Card
id = {reviews.length}
key = {reviews.length}
handleEvent = {removeItem}
name = {document.getElementById('form_name').value}
score = {document.getElementById('form_score').value}
/>]
);
}
function removeItem(id)
{
changeState(reviews.map(x => x).pop(id))
}
return (
<div id = "wrapper">
<form id = "review-form">
<h1>Review</h1>
<input className = "review-text" placeholder="Name" id = "form_name"/>
<input className = "review-text" placeholder="Score" id = "form_score"/>
<input id = "review-button" type = "Submit" onClick = {submit}/>
</form>
<ul id = "card-holder">
{reviews}
</ul>
</div>
);
}
export default App;
Review Component
import React from "react";
export default function Card(item)
{
function handle()
{
console.log(item.handleEvent);
item.handleEvent(item.id)
}
//conditional rendering with and statements
return(
<div className = "card-wrapper">
<div className = "card">
<h2>{item.name}</h2>
<h4>{item.score} / 5</h4>
</div>
<span class="material-symbols-outlined" onClick = {handle}>close</span>
</div>
);
}
Here's a simplified and fixed example. As I said in the comment, don't put elements in state; just map your data into elements when returning your markup.
The initial data is returned by a function, so we dont accidentally mutate "static data" from another module
The Card component now accepts the item as a prop, not "spread out"
Removal actually works (we filter the state array so there's only items without the ID-to-remove left)
import React from "react";
function getInitialData() {
return [
{
id: 0,
name: "Mindustry",
score: "4.5",
},
{
id: 1,
name: "Minecraft",
score: "4",
},
{
id: 2,
name: "Plants vs Zombies",
score: "4",
},
];
}
function Card({ item, onRemove }) {
return (
<div className="card-wrapper">
<div className="card">
<h2>{item.name}</h2>
<h4>{item.score} / 5</h4>
</div>
<span
className="material-symbols-outlined"
onClick={() => onRemove(item.id)}
>
close
</span>
</div>
);
}
function App() {
const [data, setData] = React.useState(getInitialData);
function removeItem(id) {
setData((reviews) => reviews.filter((item) => item.id !== id));
}
function submit(e) {
e.preventDefault();
setData((reviews) => {
// TODO: do these with refs or state instead of getElementById
const name = document.getElementById("form_name").value;
const value = document.getElementById("form_score").value;
const newReview = {
id: (+new Date()).toString(36),
name,
value,
};
return [...reviews, newReview];
});
}
return (
<div id="wrapper">
<form id="review-form">
<h1>Review</h1>
<input
className="review-text"
placeholder="Name"
id="form_name"
/>
<input
className="review-text"
placeholder="Score"
id="form_score"
/>
<input id="review-button" type="Submit" onClick={submit} />
</form>
<ul id="card-holder">
{data.map((item) => (
<Card key={item.id} item={item} onRemove={removeItem} />
))}
</ul>
</div>
);
}
Let's say I have a <SelectPicker/> component where I can select an option. What I want is how to add another <SelectPicker/> after I selected an option.
function DynamicComponent() {
const [state, setState] = useState([
{ name: null, id: '1' },
]);
const handleAdd = (value) => {
// Updating logic
};
return(
<>
{ state.map(item => {
return <SelectPicker
onSelect={handleAdd}
key={item.id}
value={item.name}
data={options} />
})
}
</>
);
}
In the example above, let's say there is default SelectPicker which is not selected. After selection, I think handleAdd function should update object that has id equal to '1' and add another object like this { name: null, id: '2' }.
What is the best way to achieve such functionality in react? Any help would be appreciated.
Thank you.
On an abstract level, what you want to do is have an array of components inside your state which is then called by the Render function of DynamicComponent. This array should get its first SelectPicker component immediately, and every time handleAdd is called you add a new SelectPicker to the array using your setState function. You can get the id for each new SelectPicker component by finding array.length.
In addition to the question, the below note from OP is also addressed in the below question
what if I want to update object's name property that has id:1 and add
new object to state at the same time?
This may be one possible solution to achieve the desired result:
function DynamicComponent() {
const [myState, setMyState] = useState([
{name: null, id: '1'}
]);
const handleAdd = arrIdx => setMyState(prev => {
const newArr = [...prev];
prev[arrIdx]?.name = ">>>>----new name goes here---<<<<";
return [
...newArr,
{
name: null,
id: (prev.length + 1).toString()
}
]
});
return(
<div>
{myState.map((item, idx) => (
<SelectPicker
onSelect={() => handleAdd(idx)}
key={item.id}
value={item.name}
data={options}
/>
)}
</div>
);
}
NOTES
Avoid using variable-names such as "state"
Passed the "index" from the .map() iteration
This helps in tracking the exact array-element
The new element with name: null is added as the last
The id is calculated by incrementing the current length by 1
I've made a shorter version of what I'm trying to create for simplicity here. I know some of the code here is wrong. I've spent hours trying different ways and can't get anything to work, so I've stripped it back.
Aim: I'm rendering a FlatList. I need to be able to update the 'qty' in each respective object in the array with the click of a button in each particular FlatList item.
So, if I click 'Increase QTY' in 'abc', then the 'qty' data will be increased by 1.
I've looked everywhere online and can't seem to be able to get any closer. Any help would be hugely appreciated.
import React, { useState } from 'React';
import { View, Text, Button, FlatList } from 'react-native';
const DataApp = () => {
const [data, setData] = useState([
{ id: 1, name: 'abc', qty: 1 },
{ id: 2, name: 'def', qty: 2 },
{ id: 3, name: 'ghi', qty: 3 },
]);
const incQuantityHandler = (data) => {
setData([...data, prevState => qty[prevState] + 1 ])
}
const Item = ({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.qty}</Text>
<Button title="Increase QTY" onPress={incQuantityHandler}/>
</View>
)
const renderItem = ({ item }) => (
<Item name={item.name} qty={item.qty} />
)
return (
<View>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
)
}
export default DataApp;
With your current setup, incQuantityHandler gets called with an event object whenever the button is pressed. You should use onClick by the way.
You can use an arrow function to pass on which button you're pressing, otherwise use a wrapper function:
onClick={() => incQuantityHandler(item.name)}
// or alternatively, but basically the same:
const wrapHandler = item => (() => incQuantityHandler(item.name));
onClick={wrapHandler(item)}
Your incQuantityHandler itself is incorrect. I suggest re-reading the React documentation and learning about array destructuring/spreading, but you probably want something like:
// Remember that now we get the item name instead
const incQuantityHandler = (itemName) => {
// Use an arrow function to mutate data
setData(data =>
// Use map to map over all items
data.map(item => {
// Leave other items the way they are
if (item.name !== itemName) return item;
// Return a modified copy of our target item
// where we changed the qty field
return { ...item, qty: item.qty + 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,
},
]);
I am trying to create a check list component in-which I can toggle the checklist items to true/false and then display this data in a separate element.
When I try my example code it throws this error: TypeError: checkList.map is not a function
I've also looked at other hooks but I don't see an obvious solution.
Example Code:
import React, {useState} from 'react';
const CheckListTest = () => {
const [checklist, setChecklist] = useState([
{name: 'option1', value: false},
{name: 'option2', value: false},
]);
return (
<>
{checklist.map((item) => {
return item.value === true ? item.name : '';
})}
{checklist.map((item) => {
return (
<div
onClick={() => {
setChecklist(...checklist, (item.value = !item.value));
}}
>
{item.name}
</div>
);
})}
</>
);
};
Example of UI:
- When clicking item from bottom list it should remove itself from the array and push to the list above. (Haven't gotten this far as I am still stuck)
Thanks in advance
You are calling the setCheckList function with the wrong numner of parameters. The setter in useState accepts one argument. Try changing the setCheckList function inside your onClick handler to this:
setCheckList(checkList => {
const newCheckList = [...checkList];
// Do someting here to change the `newCheckList` variable, e.g.:
if (!item.value) {
const index = newCheckList.findIndex(item => item.value);
newCheckList.splice(index, 1);
}
return newCheckList;
})