I was in a impression that both useState
and useReducer
works similarly except the fact that we should use useReducer when the state is complex/nested objects.
But today I found a strange behavior, I was looping over an array and setting the values to a state object. I did this same example using both useState
and useReducer
.
With useState
: It only pushes the last value from the array to the state object, as useState is async in nature, So when we setState inside a loop, it may not update properly based on previous state. So you get just the last one object inside the state.
With useReducer
: I was expecting the same behaviour with useReducer, but with useReducer, it seems to properly set the states when we dispatch
actions from inside a loop. So here you get all the objects inside the state.
useState
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
const [students, setStudents] = React.useState({});
const createStudents = () => {
const ids = [1,2,3];
const names = ['john', 'michael', 'greg']
for(let i = 0; i < 3; i++){
const student = {[ids[i]]: names[i]};
setStudents({...students, ...student})
}
}
return (
<div className="App">
<button onClick={createStudents}>Create Students</button>
<br />
{JSON.stringify(students)}
</div>
);
}
useReducer
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
const studentReducer = (state, action) => {
switch (action.type) {
case 'ADD':
return {...state, students: {...state.students, ...action.payload}};
default:
throw new Error();
}
}
const [students, dispatch] = React.useReducer(studentReducer, {students: {}});
const createStudents = () => {
const ids = [1,2,3];
const names = ['john', 'michael', 'greg']
for(let i = 0; i < 3; i++){
const student = {[ids[i]]: names[i]};
dispatch({type: 'ADD', payload: student})
}
}
return (
<div className="App">
<button onClick={createStudents}>Create Students</button>
<br />
{JSON.stringify(students)}
</div>
);
}
I was in a impression that both useState and useReducer works similarly
That impression is correct. In fact, useState
even calls the exact same code as useReducer
, so really useState
is basically a special case of useReducer
(source code).
The behavior you're seeing isn't about whether it's synchronous or asynchronous, it's about whether you're calculating the new state from the value in your closure, or if you're calculating it from the most recent value. Consider this code:
const [students, setStudents] = React.useState({});
//... some code omitted
setStudents({...students, ...student})
Note that students is a const
. It will never change, not even when you call setStudents
. If you're only setting state once, that's not really a problem: you'll make a copy of students, add in the one new student, and pass that in. The component then rerenders. On that new render, a new local variable will be created which has the new object with one student. Code in that new render can then interact with that object with one student and perhaps create one with two students.
But if you do it in a loop, then each time through the loop you're starting from the empty object. You copy the empty object, add in student A, and tell react to set state to that. students
hasn't changed, so you then copy the same empty object again, and add in student B. Note that this second time does not include A. Eventually the loop finishes, and the only setStudents that's going to matter is the very last one.
When setting state, there's another form you can use. You pass in a function, and react will call that function with whatever the latest value is:
setStudents(previous => {
return {
...previous,
...student
}
});
With this approach, previous starts off as the empty object, and you add in student A. Then on the next time through the loop, previous is now the object that includes student A, and you add in student B. So when you're done, you'll have all the students, not just the last one.
So back to your question about useState and useReducer: The reason they're different is that useReducer always uses the callback form. The reducer function will always be passed in the most recent state, and so you're calculating your new state based on that, not based on whatever students
was equal to the last time the component rendered. You can get the same thing to work in useState too, if you redo your code to use the callback as shown above.