Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React - weird behavior when useState hook sets state for the first time vs subsequent times

consider this code:

const {useState} = React;

function App() {
  const [count, setCount] = useState(0);

  const onClick = () => {
    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    console.log("onclick");
  };

  console.log("rendering");

  return <button onClick={onClick}> Increment {count} </button>;
}

ReactDOM.render(<App/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>

When run and the button is clicked, this is the output it produces:

1
onclick 
2
3
rendering 

I would have expected the output to be

onclick 
1
2
3
rendering

as I am using the updater function to access previous stat and state updates are batched and async by default.

And expectedly so, further click on the button confirm this, and produce this output:

onclick 
4
5
6
rendering

I suspect that the first set state is always synchronous in case of hooks, because if I change my code to this:

function App() {
  const [count, setCount] = useState(0);

  const onClick = () => {
    // add this extra set state before any other state updates
    setCount(1);
    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    console.log("onclick");
  };

  console.log("rendering");

  return <button onClick={onClick}> Increment {count} </button>;
}

the output as expected is:

onclick 
2
3
4
rendering 

I haven't been able to find any explanations for this online yet. This would not impact any functionality as much as I can see, because although the first time it is executed synchronously it still updates the state asynchronously, which means I can't access the updated state in console.log("onclick" + count)

Would be helpful to get an explanation of why it works like this.

NOTE: discussed this on github. Seems like this is one of the things that as consumers we should not care about. It is an implementation detail. https://github.com/facebook/react/issues/19697

like image 762
gaurav5430 Avatar asked Aug 25 '20 20:08

gaurav5430


People also ask

Does React useState hook update immediately?

React do not update immediately, although it seems immediate at first glance.

Is useState hook asynchronous?

TL;DR: useState is an asynchronous hook and it doesn't change the state immediately, it has to wait for the component to re-render. useRef is a synchronous hook that updates the state immediately and persists its value through the component's lifecycle, but it doesn't trigger a re-render.

Does useState mutate?

You may be able to mutate it, but this will not issue a “re-render” in your component, which means the new value will not be shown in your UI.

Does useState overwrite?

setState in a class component, the function returned by useState does not automatically merge update objects, it replaces them. Try it here, you'll see how the id property is lost. The ... prevState part will get all of the properties of the object and the message: val part will overwrite the message property.


1 Answers

React will merge all state changes into one update to reduce rendering as rendering costs a lot, setState is not surely updated when you call log. So you can just log("count="+(count+3)) or use useEffect.

useEffect sets a callback when state/prop change; the following snippet shows how to log count every time it changes. You can reference the docs for more information.

const {useState,useEffect} = React;

function App() {
  const [count, setCount] = useState(0);

  useEffect(()=>{
    console.log("effect!count=" + count)
  },[count])

  const onClick = () => {
    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    setCount((prevCount) => {
      console.log(prevCount + 1);
      return prevCount + 1;
    });

    console.log("onclick");
  };

  console.log("rendering");

  return <button onClick={onClick}> Increment {count} </button>;
}

ReactDOM.render(<App/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
like image 119
Josh Lin Avatar answered Oct 03 '22 08:10

Josh Lin