I have a question concerning React and how state must be updated.
Let's say we have a class Players containing in its state an array of objects called players. We want to update one player in this array. I would have done it this way:
class Players extends Component {
state = {
players: []
}
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
}
But my coworker just did it this way, and it's also working:
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
React's function setState update the players array without telling explicitly to do it. So, I have two questions:
Is it using a reference from the find function, and using it to update the players arrays ?
Is one of those ways recommended ?
Thank you all for your explanations !
The difference is that second snippet misuses setState to trigger an update because it uses playerObj dummy property. This could be achieved with forceUpdate.
Neither of these ways are correct. Immutable state is promoted in React as a convention. Mutating existing state may result in incorrect behaviour in components that expect a state to be immutable. They mutate existing player object, and new player.update value will be used everywhere where this object is used, even if this is undesirable.
An idiomatic way to do this is to use immutable objects in state:
updatePlayer = id => {
this.setState(({ players }) => ({
players: players.map(player => ({
...player,
updated: player.id === id
}));
});
}
Notice that setState is asynchronous, updater function has to be used to avoid possible race conditions.
Yes, all it's using a reference. All javascript objects are references so whenever you do a find you get a reference to the object, so mutating it will update it.
const players = this.state.players.map(player => {
return { ...player, updated: player.id === id };
});
this.setState({players: players});
As for the recommended way, you should stick with yours where you explicitly update the state variable that you care about.
Both of them are not correct, because you are mutating state.
The best way is a create a deep copy of this array ( just clone ) and after that make some changes with this cloned array
You can also use lodash _.cloneDeep();
For example
class Example extends React.Component {
state = {
players: [
{id: 0, name: 'John'};
]
};
updatePlayer = id => {
const { players } = this.state;
const clonePlayers = players.map(a => ({...a}));
const player = clonePlayers.find(playerId => playerId === id);
player.name = 'Jack';
this.setState(() => ({
players: clonePlayers
}));
}
render() {
return (
<div>some</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
well, basically they are not the same, your coworkers code is working just because he is using an old reference of the object. so, lets take a look:
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
on your function, you are creating a new array using your old array, which is the correct way of doing this.
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
here your friend is editing the reference of the object that he got using find and then saving a playerObj which is nothing more than the reference of a player from the array that you wanted to edit. after this you should notice that the new state will be something like
this.state = {
players: [p1, p2 ,p3, p4], // each p is a player.
//notice that playerObj is now a reference to some of the P on the players array
playerObj: {
...playerstuff,
updated: true
}
}
hope it helps :)
Related
I am on the newer side of React and trying to change the state of an object in an array. Currently, I am pulling the object out of the array, changing the property in that object, then adding the new object to the state again. Problem being that it sends the object to the back of the list and reorders my checkbox inputs.
const handleChange = (e) => {
if (e.target.type === "checkbox") {
// Get the role from the current state
const roleToChange = input.roles.find(
(role) => Number(role.id) === Number(e.target.id)
);
// Change checked state to opposite of current state
const changedRole = { ...roleToChange, checked: !roleToChange.checked };
// Get every role except the one that was changed
const newRoles = input.roles.filter(
(role) => Number(role.id) !== Number(e.target.id)
);
// Update the role in the state
setInput((prevState) => {
return { ...prevState, roles: [...newRoles, changedRole] };
});
}
Can I update the object in the array in-place so this doesn't happen?
Don't .filter - .map instead, and return the changed object in case the ID matches, so the new object gets put at the same place in the new array as it was originally.
const handleChange = (e) => {
if (e.target.type !== "checkbox") {
return;
};
const newRoles = input.roles.map((role) =>
Number(role.id) !== Number(e.target.id)
? role
: { ...role, checked: !role.checked }
);
setInput((prevState) => {
return {
...prevState,
roles: newRoles
};
});
}
Unless the state is updated synchronously before this, which sounds a bit unlikely (but not impossible), you can also probably use setInput({ ...input, roles: newRules }) instead of the callback.
I have 2 states product and variations I call an API and set the values of both state to the API response.
I want the product state to stay as it is and not update
const [product, setProduct] = useState({} as any);
const [variations, setVariations] = useState([] as any);
useEffect(() => {
const getProduct = async () => {
const data = await axios.get("/products?id=4533843820679");
console.log(data);
setProduct(data.data);
// #ts-ignore
setVariations([data.data]);
};
getProduct();
}, []);
In return I map the variations array and return inputs for title, and price and a button to add variations. Adding variations will add another product to variations array. So it just pushes product to variations.
Then I have inputs for title in variation and prices in variation.variants. The problem is with onChange.
When I change the price of one element in variants it changes for all and also changes it for PRODUCT state.
The code can be found here: https://codesandbox.io/s/smoosh-firefly-6n747?file=/src/App.js
Add variations, change prices add another variations and you'll see all issues I'm facing.
It is because of this:
variant.price = e.target.value; // same issue with title
the variant object reference is shared among variations and you are modifying it directly. It is shared because you you made a shallow copy of a variation using ... when adding it.
Here is the solution:
You should update the specific variant object in immutable way (in react you should always update state in immutable way). For that you need to use this as onChange for price:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
variants: x.variants.map((y) => {
if (y.id === variant.id) {
return {
...y,
price: e.target.value
};
}
return y;
})
};
}
return x;
});
setVariations(updated);
}
}
This for onChange for title:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
title: e.target.value
};
}
return x;
});
setVariations(updated);
}
}
NOTE but ids of variations must be different. For testing purposes you can use this as click handler when adding a new variation:
onClick = {
() => {
setVariations((prev) => [...prev, {
...product,
id: Math.floor(Math.random() * 1000) // for testing
}]);
}
}
First, you are not pushing the product to variations. You are overwriting it.
To push a value to array with useState,
setVariations([...variations, product])
But, if you change the product object, variations also gonna be change because it's the same object. (Maybe, react not gonna re-render it but trust me, it is changed.) If you want to keep it same you need to create new object.
So,
setProduct(data.data);
setVariations([...variations, {...data.data}]);
Now, you can change product. variations not gonna change.
This was because you did a shallow copy of an object.
Try to do like this:
setVariations([...variations, data.data,]);
Long story short, I have a class component that constructs a poll. Before sending the data to the server I need to transform it a little so it fits the API request. I created a transformData method on my class component that transforms the data derived from the state. As a side effect it sets the data in separate this.state.data property so I can attach it with the API request. The problem is that the method mutates the other properties of the state.
transformData = () => {
const { title, sections } = this.state
const transformedSections = sections.map(section => {
delete section.isOpen
const transformedQuestions = section.questions.map(question => {
question.label = question.question
question.type = toUpper(question.type)
delete question.question
return question
})
section.questions = {
create: transformedQuestions,
}
return section
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
}
So I get this:
state: {
data: {...} //our transformed data
sections: {...} //transformed as well!!
}
instead of getting this:
state: {
data: {...} //our transformed data
sections: {...} //same before calling the method
I re-wrote the method with different approach — basically replaced all Array.map with Array.forEach and it worked as expected.
transformData = () => {
const { title, sections } = this.state
const transformedSections = []
sections.forEach(section => {
const transformedQuestions = []
section.questions.forEach(question => {
transformedQuestions.push({
label: question.question,
type: toUpper(question.type),
max: question.max,
min: question.min,
instruction: question.instruction,
isRequired: question.isRequired,
placeholder: question.placeholder,
})
})
transformedSections.push({
title: section.title,
questions: { create: transformedQuestions },
})
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
Can anyone explain what's going on here? How can I accidentally mutate a state property without explicitly calling this.setState on the aforementioned property? The thing is that the originally written method mutates the state even if I return the data object without calling this.setState whatsoever. Like so:
//This still mutates the state
return {
data: {
title,
sections: { create: transformedSections },
}
}
//without this!
//this.setState({
// data: {
// title,
// sections: { create: transformedSections },
// },
// })
Thanks!
javascript behave like this way,
its called variable referencing.
it works like pointer variable in C.
if your console those variable such as console.log(var1 == var2) it will show true cuz both references from same memory location
if you want to prevent mutate original variable then you have to create another brand new variable to mutate
like this way :
const { title, sections } = this.state
// create new variable following old one (spreading es6 way)
const tempSections = [...sections]
...
also
sections.forEach(section => {
const transformedQuestions = []
const tempQuestions = [...section.questions]
tempQuestions.forEach(question => {
...
always have to create a brand new variable of object/array/... to prevent auto mutation
for further info here
Issue here is of Shallow Copying :
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
// state is muate already
console.log("---- After map -----" , this.state);
You can run the below code snippet and check both console.log, and check for "isOpen": true
Hope this will clear all your doubts :
const { useState , useEffect } = React;
class App extends React.Component {
state = {
title : "questions" ,
sections : [{
isOpen : true ,
questions : ["que1" , "que2" , "que3"]
}]
}
transfromData = () => {
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
console.log("---- After map -----" , this.state);
}
render() {
return (
<div>
<button onClick={this.transfromData}>transfromData</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
You should never update the state without using the setState method. It is asyncronous, and if you don't set it properly you never know what might happen - and that's what you're seeing in the first part of your answer. See the docs
By doing
section.questions = {
create: transformedQuestions,
}
you are improperly altering the state, so you'll see this.state.sections transformed as well, because each element inside this.state.sections has now an attribute questions that contains create with the value transformedQuestions
In a previous question, I was given an answer on how to update an array, which was achieved in the following way:
onClick(obj, index) {
if (data.chosenBets[index]) {
// Remove object.
data.chosenBets.splice(index, 1);
} else {
// Add object.
data.chosenBets.splice(index, 0, obj);
}
}
This does not trigger a re-render in my UI. How do I update the array (in the same way as above) while triggering a re-render?
Just mutating a state won't trigger re-render. You need to call setState() function:
// class component
onClick = () => {
// update state
this.setState(newState);
}
// functional component
...
const [ state, setState ] = useState();
...
setState(newState);
Also, it's quite important to perform immutable state updates since React relies on refs usually (especially, when using memo() or PureComponent). So, it's better to create new instance of array with the same items.
onClick(obj, index) {
let newData;
if (data.chosenBets[index]) {
newData = data.slice();
newData.chosenBets.splice(index, 1);
} else {
newData = [ obj, ...data ];
}
setState(newData);
}
And you always can use some libraties for immutable update like immer, object-path-immutable etc.
Try avoiding impure functions when writing react codes. Here, splice is an impure method. I would recommend using the below code:
onClick(obj, index) {
if (this.state.data.chosenBets[index]) {
// Remove object.
const data = {
...this.state.data,
chosenBets: this.state.data.chosenBets.filter((cBet, i) => i !== index)
};
this.setState({ data });
} else {
// Add object.
const data = {
...this.state.data,
chosenBets: [ ...this.state.data.chosenBets, obj ]
};
this.setState({ data });
}
}
I am assuming you have that array already saved in your state. Then you can do something like this:
onClick = (idx) => {
let arr = [...this.state.arrayToModify];
arr.splice(idx,1);
this.setState({ arrayToModify: arr });
}
Hope this helps!
I needed to make a copy of the array:
let arr = appState.chosenBets
arr.splice(index, 1)
appState.chosenBets = arr
Rather than simply doing
data.chosenBets.splice(index, 1);
I have a todo list that holds a delete button in a grandchild, that when clicked fires an event in the parent - I am wanting this event to delete the array entry corresponding to the grandchild clicked.
Parent (contains the array and my attempt at the function)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
// taskToDelete is the name of the task - doesn't contain an object
deleteTask(taskToDelete) {
this.state.tasks.remove(task => task.name === taskToDelete);
this.setState({ tasks: this.state.tasks });
}
Any help would be appreciated
Two issues there:
You're seeming to try to direct modify this.state.tasks. It's important not to do that, never directly modify this.state or any object on it. See "Do Not Modify State Directly" in the React documentation for state.
You're passing an object to setState that is derived from the current state. It's important never to do that, too. :-) Instead, pass setState a function and use the state object it passes you when calling that function. From "State Updates May Be Asynchronous" in the documentation:
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state... [Instead]...use a second form of setState() that accepts a function rather than an object.
(my emphasis)
I figure your remove on an array was intended to be hypothetical, but for the avoidance of doubt, arrays don't have a remove method. In this case, the best thing to do, since we need a new array, is to use filter to remove all entries that shouldn't still be there.
So:
deleteTask(taskToDelete) {
this.setState(prevState => {
const tasks = prevState.tasks.filter(task => task.name !== taskToDelete);
return { tasks };
});
}
You could simply filter the array :
this.setState(prevState => ({
tasks: prevState.tasks.filter(task => task.name !== 'taskToDelete')
}));
Also when updating based on this.state, its better to use the function form because setState is async.
You can use filter to remove one object from an array following the immutable pattern (filter will create a new array) :
deleteTask(taskToDelete) {
const newTaskArray = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: newTaskArray });
}
Edit : codepend of the solution : https://codepen.io/Dyo/pen/ZvPoYP
You can implement deleteTask method as below:
deleteTask(taskToDelete) {
this.setState((prevState, props) => {
const tasks = [...prevState.tasks];
const indexOfTaskToDelete = tasks.findIndex(
task => task.name === taskToDelete
);
tasks.splice(indexOfTaskToDelete, 1);
return { tasks };
});
}
A. Find the index of taskToDelete.
B. Then use splice method to delete the item from the collection
C. Then call setState to update the state with tasks.
You can use higher order function Array#filter to delete the task.
let updatedTasks = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: updatedTasks });
I have followed below steps to delete a particular selected Object from the state array:
Here I am using a list of checkBoxes, when I am selecting a checkBox it will add it in the state array and when it gets de-selected then it will get deleted from the array.
if (checked) {
var tempObject = { checkboxValue: data, label: title }
this.state.checkBoxState.push(resTemp);
} else {
var element = data; //data is coming from different method.
for (let index = 0; index < this.state.checkBoxState.length; index++) {
if (element === this.state.checkBoxState[index].checkboxValue) {
this.state.checkBoxState.splice(index, 1);
}
}
}
I got stuck for this question and I am sharing my solution. Hope it will help you.