What is the difference between those two state updates in react? - javascript

I'm studying a React course at a point where the instructor is explaining about updating states and I cannot understand how those two snippets are really different internally, please see in the codepen links below:
Updates the state directly snippet
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
handleIncrement = counter => {
const updatedCounters = [...this.state.counters];
updatedCounters[0].value++;
};
}
Updates the state indirectly then save snippet
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
handleIncrement = counter => {
const updatedCounters = [...this.state.counters];
const index = updatedCounters.indexOf(counter);
updatedCounters[index] = {...counter};
updatedCounters[index].value++;
this.setState({ counters: updatedCounters });
};
}
In this lecture, the instructor explains that the first snippet does update the state directly.
So my question is, if the first example, as the instructor says, updates the state directly, the only thing that prevents the second snippet from updating the state directly is the line below?:
updatedCounters[index] = {...counter};
If that is true, how does it works?

In the first example, updatedCounters is a copy of this.state.counters, but the items inside that copy are references to the exact same objects in the original. This might be analogous to moving some books to a new box. The container changed, but the contents did not. In the second example, you don't mutate the selected counter, you copy the counter and then mutate that copy.

When it comes to state modifications in React, You always need to remember about the fact that the state can not be mutated. Well , technically You can do that but it's a bad practice and it's a antipattern. You always want to make a copy of the state and modify the copy instead of the original state object. Why ? It really improves the performance of the application and that's React's huge advantage. It's called immutability.
You might also ask .. "How does this approach improve the performance?"
Well, basically, thanks to immutability pattern, react does not have to check the entire state object. Instead of that, React does a simple reference comparison. If the old state isn't referencing the same obj. in memory -> we know that the state has changed.
Always try to use .setState() to avoid mutating state in a wrong way and that's what you do here:
handleIncrement = counter => {
const updatedCounters = [...this.state.counters];
updatedCounters[0].value++;
};

Basically the answer to my own question is that, if you do not call setState react wont trigger the necessary routines to update the Component "view" value on screen (mentioning the first example)

Related

Is it a bad practice to set property of object, nested in another object by directly setting it on destructured copy of state?

I've found an article which states that if i want to change property name in such state:
const [user, setUser] = useState({
name: 'Cody',
age: 25,
education: {
school: {
name: 'School of Code'
}
}
})
i need to do following:
setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
})
Howewer, they later show that it is possible to make this logic simpler, using immer.js (also changing useState on useImmer), like this:
setUser(draft => {
draft.education.school.name = 'Layercode Academy';
})
My question is whether i can do this, without using immer.js:
setUser(prevUser => {
const newUser = {...prevUser}
newUser.education.school.name = 'Layercode Academy'
return newUser
})
In every tutorial i've seen (that doesn't use immer.js), they do destructuring. But just assigning value to property of state copy seems simpler and more concise for me in many situations. I am not setting state directly, but rather just modify copy, which is not breaking any "rules" . Are there some hidden pitfalls?
Here is the difference between the copies
SHALLOW COPY
const a = { x: 0, y: { z: 0 } };
const b = {...a}; // or const b = Object.assign({}, a);
b.x = 1; // doesn't update a.x
b.y.z = 1; // also updates a.y.z
DEEP COPY
const a = { x: 0, y: { z: 0 } };
const b = JSON.parse(JSON.stringify(a));
b.y.z = 1; // doesn't update a.y.z
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. - Dan
So with this in mind, you don't have to deep clone all things as it can be very expensive and cause unnecessary renders.
so if you have to do something with the data after the render
setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
})
doSomething(user) // setUser merely schedules a state change, so your 'user' state may still have old value
}
Instead,
setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
}, (user) => { doSomething(user})
This way you guarantee the update of the user and don't have to worry about the mutation.
Are there some hidden pitfalls?
Yes, though we can't know if the rest of your code will experience them. So in general just consider it as a blanket "yes".
What you're doing is mutating state. In cases where the entire operation is just performing one state update and re-rendering, you're unlikely to experience a problem. But it's still building upon and relying upon a bad habit.
In cases where more operations are being performed (multiple batched state updates, other logic using current state before state updates are processed, etc.) then you're much more likely to see unexpected (and difficult to trace) bugs and behaviors.
The overall rule of "don't mutate state" is just that, an overall rule. There are indeed cases where one can mutate state without causing any problems. But why? Why rely on bad habits solely because a problem hasn't occurred yet?
A state update should be a wholly new object (or array, or just plain value). For object properties which don't change, it can still be a reference to the previous object property (which deconstructing will do). But for any property which does change, the entire object graph which leads to that property should be new.
Yep, there is something wrong with your last example. You're still modifying the original user's education object:
setUser(prevUser => {
const newUser = {...prevUser}
newUser.education.school.name = 'Layercode Academy'
// true
prevUser.education.school.name === newUser.education.school.name
return newUser;
});
You may consider breaking up state to multiple states (that are primitives):
const [userEducationSchoolName, setUserEducationSchoolName] = useState("School of Code");
but this gets out of hand very quickly. If your environment supports it, you may even use structuredClone:
setUser(prevUser => {
const newUser = structuredClone(prevUser);
newUser.education.school.name = 'Layercode Academy'
return newUser
});
but since some browsers don't support structuredClone yet, you would need some other way to deep clone an object. And this brings us back to Immer! The whole point of Immer is to give you a mutable deep copy of the state for you to change.

What do you suggest to make undo?

I use a series of data as follows:
[
{
name: 'name1',
background:'red',
child:[
{
name:'',
id:'',
color:'',
text:'',
border:''
},
{
name:'',
id:'',
color:'',
text:'',
border:''
}
]
},
{
name: 'name2',
background:'red',
child:[
{
name:'',
id:'',
color:'',
text:'',
border:''
},
{
name:'',
id:'',
color:'',
text:'',
border:''
}
]
}
]
I'm going to save all the changes to another variable, and I used a deep copy to do that, but when I log in, the variables are the same.I need to children all the children changes too.
I wrote it in Reducers
const Reducers =(state = initialState, action) => {
switch (action.type) {
case NEW_OBJECTS_PAST:
const OldPast = JSON.parse(JSON.stringify(state.past))
const newDate = JSON.parse(JSON.stringify(state.present))
// const newDate = _.cloneDeep(state.present);
const newPast = [
OldPast,
newDate
];
return {
...state,
past : _.cloneDeep(newPast) ,
}
case OBJECTS:
return {
...state,
present: action.objects,
// future:[]
}
Do you suggest another way to build undo in REACT and REDUX ?
I tried the libraries available for this, but got no answer.
First two remarks :
you should never deep clone parts of your state, it doesn't bring you any benefits as the state is immutable anyway, but is very detrimental for memory usage and for performance (both when doing the deep cloning and then when using the deep cloned state),
you should use Redux toolkit, which makes it way easier to write immutable reducers and prevent many errors.
To implement undo, I'm not sure what your actions are supposed to mean but you can do it as follows
the state contains state.present (the current state) and state.past (an array of past states)
whenever you do an action that you want to undo, you push the current state.present at the end of state.past and compute the new state.present
whenever you want to undo, you pop the last element of state.past and put it in state.present.
In your code I can't see any undo action, and you're also building nested arrays because of new Past = [oldPast, new Date], you most likely meant to spread oldPast.

Changing the state doesn't work as expected

I created two functions that change the state:
class App extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 0 },
{ id: 4, value: 4 },
],
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
...
above code works and change the state, I then created slightly shorter form of above function handleIncrement but it didn't work
handleIncrement = (counter) => {
this.setState({
counters: this.state.counters[this.state.counters.indexOf(counter)]
.value++,
});
in above approach I used setState and didn't change the state directly. So what is the problem with it?
Your "slightly shorter form" does something completely different than the original code. this.state.counters is an array of objects. In your first example, you correctly update that array by changing the value in one of the objects in the array. In your second example, you replace the array with the result of this.state.counters[this.state.counters.indexOf(counter)].value++ which is a number not an array.
You probably meant to do something like this instead:
handleIncrement = (counter) => {
this.state.counters[this.state.counters.indexOf(counter)].value++;
this.setState({
counters: this.state.counters,
});
This increments the value inside the array and then calls setState() by passing in the array for the key counters. However, mutating state directly like this is considered poor practice in React because it is easy to forget to call setState() to initiate rendering our components. Instead, we create a copy and update the copy and pass that to setState().

React -Update a nested object in redux state of an unknown depth

When a user loads a blog post page, a post is fetched and its comments and subcomments are stored in the Redux state with the following structure:
forumPost: {
id: #,
data: {object},
comments: [
{
id: #,
data: {object},
comments: [
{
id: #,
data: {object},
comments: [...]
},
...
]
},{
id: #,
data: {object},
comments: [...]
},
...
}
When a user posts a new comment/subcomment, it is posted to the database and locally added to the Redux state. I am having problems with the second part
I created a function for finding the path to the parent comment of the comment being posted but am having trouble injecting the new comment into the current state.
const findPath = (comments, parentId) => {
let path = [];
const findIndex = (arry, count) => {
for (; count < arry.length; count++) {
if (arry[count].id === parentId) {
path = "forumPost.comments[" + count + "].comments";
return true;
}
if (arry[count].comments && arry[count].comments.length > 0) {
if (findIndex(arry[count].comments, 0)) {
path = path
.split("forumPost")
.join("forumPost.comments[" + count + "]");
return true;
}
}
}
};
findIndex(comments, 0);
return path; //Returns the parent's path as 'forumPost.comments[a].comments[b].comments'
};
I can create an entirely new state, add the new comment, and replace the old state, but doing so is expensive and (as I understand it) makes it impossible for a component to tell if the Redux state (mapped to props) has changed.
So, having done some research I have come to understand that redux treats updating and replacing the state differently and what I want to do is update the state.
https://reactjs.org/docs/update.html
Cleaner/shorter way to update nested state in Redux?
Object spread vs. Object.assign
I’ve tried to use .update(), …spreads, and dot-prop, but haven’t been able to get them to work (most likely because I have been using them wrong)… and that’s why I am here. How do you update the state with a new comment object to one of the comments arrays of an unknown depth?
Also, I now know that its best practice to keep your redux state as shallow as possible. But the current structure is what it is.

Deep merge of complex state in React

When I have the following initial state declared:
getInitialState: function() {
return {
isValid: false,
metaData: {
age: 12,
content_type: 'short_url'
}
};
},
and I update state with setState like this:
...
let newMetaData = { age: 20 };
...
this.setState({
isValid: true,
metaData: newMetaData
});
...
Resulting this.state.metadata object has only age defined. But as far as I'm aware, this.setState() merges it argument to existing state. Why it's not working here, isn't this supposed to be recurrent merging?
Is there a way to merge new object properties to state object property in React/ES6?
setState performs a shallow merge. If metaData is is flat:
this.setState({
metaData: Object.assign({}, this.state.metaData, newMetaData),
});
or if using spread :
this.setState({
metaData: { ...this.state.metaData, ...newMetaData },
});
Another way to approach this, if you only need to update one property, would be like this:
this.setState({
metaData: {
...this.state.metaData,
age: 20
}
})
setState can also take a function, which receives an argument of state, and you can use lodash merge to do the deep merge.
setState(state => merge(state, yourPartialObjectToBeDeepMerged));
a tricky solution is here
const [complexObject, setComplexObject] = useState({a:{b:{c:1}}})
setComplexObject((s)=>{
s.a.b.c = 2
return {...s}
})
How does it work?
An object is a reference. If you s.a.b.c = 2, this will update the state, but the component doesn't rerender, so the dom will not change.
Then we need a way to rerender the component. If you setComplexObject(s=>s), it will not trigger rerender, because albeit the interior of the ComplexObject do change, the reference still point at the same object.
So we need ES6 spread operator to reconstruct a object by doing this {...s}

Categories