Case 1:
const [present, setPresent] = useState([]);
useEffect(() => {
for (var j = 1; j <= totalPeriod; j++) {
setPresent([
...present,
{
period: j,
present: true,
},
]);
}
}, []);
Case 2:
const [present, setPresent] = useState([]);
let createPresent = [];
for (var j = 1; j <= totalPeriod; j++) {
createPresent = [
...createPresent,
{
period: j,
present: true,
},
]
}
useEffect(() => {
setPresent(createPresent);
}, []);
When I am trying to update the present state using loop in inside useEffect() in Case 1, present state is not updating. But when I am separately using loop outside the useEffect() and creating an array which I am then assigning to present state in case 2, the present state is getting updated.
What is the reason behind this? Why present state is not updating in Case 1?
In the below case, your present state is not the result of each subsequent state update but rather the initial one which you had which is []. React will batch these updates and not make them happen synchronously so effectively there will be just one state update with present updated to latest entry in your for loop.
const [present, setPresent] = useState([]);
useEffect(() => {
for (var j = 1; j <= totalPeriod; j++) {
setPresent([
...present,
{
period: j,
present: true,
},
]);
}
}, []);
In the below case, you are first assembling a createPresent array with all the values you need and finally calling the state updator function i.e. setPresent to set the state.
const [present, setPresent] = useState([]);
let createPresent = [];
for (var j = 1; j <= totalPeriod; j++) {
createPresent = [
...createPresent,
{
period: j,
present: true,
},
]
}
useEffect(() => {
setPresent(createPresent);
}, []);
In order to achieve the second behaviour with first, you can make use of the state updator callback which holds the previous state as the argument like so :-
const [present, setPresent] = useState([]);
useEffect(() => {
for (let j = 1; j <= totalPeriod; j++) {
setPresent(prevState=>[
...prevState,
{
period: j,
present: true,
},
]);
}
}, []);
Here also state update is batched but previous state is factored in before updating the next one.
When I say a batched state update, I mean there will only be a single render. You can verify that by doing console.log('render') in your component's function body.
Note the use of let instead of var here since let is scoped you will get the accurate value for the variable j.
The assumption you're making in Case 1 is that the setPresent call is synchronous and that it updates present immediately, while in reality state updates in React are almost always asynchronous. Thus the present variable inside for (var j = 1; j <= totalPeriod; j++) { will be equal to the original value, which is an empty array []. So in essence, you're setting the state to this over and over:
[
...[],
{
period: j,
present: true,
},
]
This will result in your state being updated to the last array the for loop creates. So if totalPeriod is equal to 5, your state will end up being [{period: 5, present: true}] instead of [{period: 1, present: true}, {period: 2, present: true}, ... ]
In Case 2, everything is very straightforward, you assemble the array without messing with state variables and then set the state in one shot. Just like you're supposed to in this case.
In addition to other answers:
You need to keep some key points in your mind while updating states in React component.
First of all updating state in React state is not synchronous, hence will be batched unless you trigger it asynchronously.
state = { count: 0};
increment() {
this.setState({ count: this.state.count + 1});
this.setState({ count: this.state.count + 1});
this.setState({ count: this.state.count + 1});
console.log(this.state.count) // 0
}
increment()
console.log(this.state.count); // 1
And, the final value of this.state.count will be 1 after completion of the calling incemenent()
Because React batch the all calls up, and figure out the result and then efficiently make that change. Kind of this pure JavaScript code, merging where last one wins
newState = Object.assign(
{},
firstSetStateCall,
secondSetStateCall,
thirdSetStateCall,
);
So, we can say here everything has to do with JavaScript object merging. So there's another cool way, where we pass a function in setState instead of object.
Secondly, in case of react hooks useState, the updating function(second element of the array returned by useState()) doesn't rerender on the content's change of the state object, the reference of the state object has to be changed. For example: here I created sandbox: link
had I not changed the reference of the array (which is the state), it would not rerender the components.
for (let key in newState) {
newStateToBeSent[key] = newState[key]; // chnaging the reference
}
Related
The if statement in canBookSlot() is only checked once for some reason. The second time canBookSlot() is triggered, the userDetailsObj.canBook should be 0 after running updateUser(). And according to the console log it is the case, but the if statement still runs, why?
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const updateUser = () => {
userDetailsObj["hasBooked"] = 1;
userDetailsObj["canBook"] = 0;
};
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
After each render userDetailsObj will take that value from localStorage. That's how every variable inside a component which isn't a state made with useState hook, or a ref made with useRef hook behaves. You can fix your problem this by using a state, like so:
const [userDetails, setUserDetails] = useState(JSON.parse(localStorage.getItem("userDetails")));
const updateUser = () => {
const newUserDetails = { ...userDetailsObj, hasBooked: 1, canBook: 0 };
setUserDetails(newUserDetails);
localStorage.setItem("userDetails", JSON.stringify(newUserDetails));
};
const canBookSlot = (id) => {
if (userDetails.canBook != 0) {
//Always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
Can you clarify where this code runs? Are you using a class component or functional component? Would you mind sharing the entire component? If it is doing what I think it is doing, the let userDetailsString = localStorage.getItem("userDetails"); is running every render which means on every render, it grabs the value in localStorage and uses that, rather than using your object stored in userDetailsObj.
If you are using functional components, you could fix this by using state.
let userDetailsString = localStorage.getItem("userDetails");
let [userDetailsObj, updateUserDetailObj] = useState(JSON.parse(userDetailsString));
const updateUser = () => {
let u = { ...userDetailsObj,
hasBooked: 1,
canBook: 0,
}
updateUserDetailObj(u);
};
If you are using class Components, let me know and I'll update it with that option.
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().
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)
I'm loading data from saved session using:
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
this.setState({
cartItems: JSON.parse(localStorage.getItem('savedData')),
totalPrice: this.getPriceOnLoad(),
totalItems: this.getItemsOnLoad(),
});
}
}
cartItems is an array of objects. Which seems is updated before
this.getPriceOnLoad();
this.getItemsOnLoad();
functions are called, for example this.getPriceOnLoad function:
getPriceOnLoad() {
let itemsPrice = 0;
for (let i = 0; i <= this.state.cartItems.length - 1; i++) {
itemsPrice += this.state.cartItems[i].quantity * this.state.cartItems[i].price;
}
return itemsPrice;
}
but, in getPriceOnLoad function, this.state.cartItems.length is equal to 0, so for loop is not executing. I can see in React dev tools that this array has some length. Is it because componentDidMount() is executing state change synchronously and can't see updated array immediately? So my question is how could i update price and quantity of items after array is initialized?
Your state is not updated in order. For this, you could store the cartItems in a temporary value, and send it to each functions :
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems, //Short syntax for 'cartItems: cartItems'
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
}
}
You could also make your function significantly shorter by using reduce:
this.setState({
cartItems,
totalPrice: cartItems.reduce((total, item) => total + (item.quantity * item.price), 0),
totalItems: cartItems.reduce((total, item) => total + item.quantity, 0),
});
Can you show us your second function too ? It may be optimized as well. Done.
the wrong thing that you are doing is trying to use values from the state on your functions that will define your state.
you have 2 approaches to solve this:
1) use the callback function from setState and then set the state again with the new data (which into my opinion is not the best approach)
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems
}, ()=> {
this.setState({
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
})
}
}
2) send the values to your functions
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const savedCartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems,
totalPrice: this.getPriceOnLoad(savedCartItems),
totalItems: this.getItemsOnLoad(savedCartItems),
});
}
}
getPriceOnLoad() is executed before this.setState is executed. So you cannot refer to this.state in getPriceOnLoad().
When you call this.setState({}), JS first needs to generate the object for the setState() function. Means the functions you are referring to run first, then this.setState().
And in any case this.setState() is an asynchronous function, so this.state is not directly available after setState() execution.
state default values
state = {
moveType: {
value: 0,
open: false,
completed: false
}
};
// callback to update new state
let step = 'moveType';
let val = 3; // new value
let newObj = { ...this.state[step], value: val };
console.log(newObj);
this.setState({[step]: newObj }, function () {console.log(this.state);});
console.log(newObj) shows new values proper, but this.state still shows old values.. can you tell me what i'm doing wrong?
Setting state in react is pretty sensitive thing to do.
The best practices I've used to is always control object deep merge manually and use this.setState(state => { ... return new state; }) type of call, like in this example:
this.setState(state => ({
...state,
[step]: { ...(state[step] || {}), ...newObj },
}), () => console.log(this.state));
SNIPPET UPDATE start
[step]: { ...state[step], ...newObj }
Changed to:
[step]: { ...(state[step] || {}), ...newObj }
To deal correctly with cases, when state does not have this step key yet
SNIPPET UPDATE end
Thing is, that when you use this.state (in let newObj = { ...this.state[step]), it might have an outdated value, due to some pending (not merged yet) changes to the state, that you've called just couple of milliseconds ago.
Thus I recommend to use callback approach: this.setState(state => { ... use state and return new state;}) which guarantees that the state you use has latest value