useState's set state renders component immediatelly when called externally

Viewed 36

Consider the following component with the increase method that updates two states in a row. By clicking the first button I see the expected behaviour in the console

rendered
a changed
b changed

that the component is rendered only once, after the two updates. However, by clicking the second button, I get the following output

rendered
a changed
rendered
b changed

It looks like that after every set state method of useState the render is triggered.

Can you explain to me why this is happening?

I am using React 17 in production mode.

function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  const increase = useCallback(() => {
    setA(a + 1);
    setB(b + 1);
  }, [a, b]);

  console.log("rendered");

  useEffect(() => console.log("a changed"), [a]);
  useEffect(() => console.log("b changed"), [b]);

  useEffect(() => {
    window.increase = increase;
  }, [increase]);


  return <>
    <button onClick={increase}>increase immediately</button>
    <button onClick={() => setTimeout(increase, 1000)}>increase in 1s</button>
  </>;
}
1 Answers

It's because Reactjs's update batching mechanism


When updating state within an eventHandler like onChange, onClick,.etc.., React will batch all of setState into one:

<button onClick={increase}>increase immediately</button>

On the other hand, updating state outside an eventHander or wrapped inside a promise or a setTimeout in your case, they will not be batched:

<button onClick={() => setTimeout(increase, 1000)}>increase in 1s</button>

So your state will be updated synchronously, the first update causes a re-render and so on...

But this behaviour will be changed in Reactjs 18 as mentioned here

Related