skip to Main Content

I was bug fixing the FE code for my company’s App which still uses React 17, and I notice different behaviour when setting the state between React 17 and React 18 (without strict mode)

Code:

import React, { useEffect, useState } from 'react';

export default function App(props) {
  const [state, setState] = useState(0);

  console.log('render', state);

  useEffect(() => {
    (async () => {
      for (let i = 0; i < 3; i++) {
        await new Promise((res) => {
          setTimeout(res, 500);
        })

        console.log('before set state');
        setState(val => val + 1);
        console.log('after set state');
      }
    })();
  }, []);

  return (
    <h1>{state}</h1>
  );
}

Console output:

React 17

render 0
before set state
render 1
after set state
before set state
render 2
after set state
before set state
render 3
after set state

React 18

render 0
before set state
after set state
render 1
before set state
after set state
render 2
before set state
after set state
render 3

It seems that in React 17, the setState synchronously triggers the re-render, which explains why we saw render {num} before the console output after set state. However, in React 18, we could see that the re-render happens after the console before set state after set state, which means the setState didn’t immediately trigger the re-render.

Can someone explain what’s happening here? Is this due to React 18’s concurrent mode? Is there anything we should adjust when migrating from React 17 to React 18 due to this change of behaviour?

Codesandbox:

2

Answers


  1. React 18 made changes to setState so that multiple can be batched into a single render. React 17 had batching too, but it could only work for synchronous code that happened in a react lifecycle event (eg, useEffect), or a dom event (eg, onClick). React 18 expanded that functionality, so now it works in your async function.

    As a result, the renders caused by your example no longer happen synchronously. Instead react waits to see if the remaining synchronous code will set any other states.

    https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching

    Is there anything we should adjust when migrating from React 17 to React 18 due to this change of behaviour?

    Typically no changes are needed; it just improves your performance by combining 2 renders into one (your specific example won’t combine any). If you’re using class components there are some edgecases which can break your code. See this post for details: https://github.com/reactwg/react-18/discussions/21#discussion-3385721

    If you want to force a render to happen synchronously you can, but this is very rarely needed and i do not recommend it:

    import { flushSync } from 'react-dom';
    // ...
    console.log('before set state');
    flushSync(() => {
      setState(val => val + 1);
    })
    console.log('after set state');
    
    Login or Signup to reply.
  2. The key difference in setState behavior between React 17 and React 18 lies in batching. This refers to React’s ability to combine multiple state updates into a single re-render, improving performance.

    React 17:

    Batching primarily occurred within event handlers.
    If you made multiple state updates within the same event handler, React would group them and trigger a single re-render.
    Updates outside event handlers, like those triggered by asynchronous operations (e.g., setTimeout), wouldn’t necessarily be batched, leading to potentially unnecessary re-renders.

    React 18:

    Introduces Automatic Batching, which significantly expands the scope of batching.
    In most cases, even state updates outside event handlers (like those within setTimeout) will be batched, leading to fewer re-renders and improved performance.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search