Is React's useReducer is synchronous compared to asynchronous nature of useState?
Asked Answered
B

2

6

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>
  );
}
Bernardinebernardo answered 26/10, 2020 at 13:39 Comment(0)
K
13

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.

Kamacite answered 26/10, 2020 at 13:52 Comment(0)
T
0

You are correct that useState and useReducer work similarly. but I think you misunderstood the batch update nature of the react state.

when we use useState to update the same state on the same function multiple times then react batch updates the state instead of updating the state separately.

Let's see the example that Reacts authority has used on the React's docs:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

Notice that the number only increments once per click!

Here the value is not getting updated before the next render. as mentioned in the React docs:

Setting state only changes it for the next render. During the first render, number was 0. This is why, in that render’s onClick handler, the value of number is still 0 even after setNumber(number + 1) was called.

This is what is happening on your code where you have used useState.

But to stop this batch update nature we can use setState in this way:

setNumber(n => n + 1) 

On the other hand, when using useReducer, things get changed inside the studentReducer.

Here in this function, the first argument is state and this is the updated value of your students state. that's why here you can get the updated value.

 const studentReducer = (state, action) => {
    switch (action.type) {
      case 'ADD':
        return {...state, students: {...state.students, ...action.payload}};
      default:
        throw new Error();
    }
  }

Here this reducer's first argument state is doing the same thing that n argument was doing inside the setNumber(n=>n+1).

But normally useReducer also batch updates the state just like useState.

You can verify this statement by calling the dispatch function twice at the same time.

 import React from 'react';
export default function App() {
  const numberReducer = (state, action) => {
    switch (action.type) {
      case 'Increase':
        return action.payload + 1;
      default:
        throw new Error();
    }
  }

  const [number, dispatch] = React.useReducer(numberReducer, 0);

  const updateNumber = () => {
    dispatch({type: 'Increase', payload: number})
    dispatch({type: 'Increase', payload: number})
  }
  return (
    <div className="App">
      <button onClick={updateNumber}>Create Students</button>
      <br />
      {number}
    </div>
  );
}

Here you will see that inside the second dispatch payload is getting the current state instead of getting the updated state, just like the useState. and that's why the value will be increased by just 1 even though we have called the dispatch twice.

Titicaca answered 23/1 at 8:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.