DEV Community

Shivam Jha
Shivam Jha

Posted on

Batching in React

Batching In React - Banner
One might think that React's useState hook is the simplest hook. Yet, there are some complexities.

What is batching ?

Batching is when multiple calls to setState are grouped into only one state update

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

  useEffect(() => {
    // only output once per click
    console.log({ count, flag });
  }, [count, flag]);

  const handleClick = () => {
    // Here, react will re-render only once
    // Hence, the state updates are `batched`
    setCount(c => c + 1);
    setFlag(f => !f);
  };

  return (
    <div className='App'>
      <button onClick={handleClick}>Click Me!</button>
      <h3 style={{ color: flag ? 'blue' : 'black' }}>Count: {count}</h3>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why Batching ?

  • Great for performance, since avoids un-necessary re-renders.
  • Prevents any component from rendering "half-applied" state updates, which may lead to bugs.

Inconsistent Batching Behavior

However, React was (more about that later) not consistent about batching. For example, in an async function / promise based API,
React would not batch the updates & independent updates would happen (performing two setState calls).

// little async function
const sleep = () => new Promise(resolve => setTimeout(resolve, 200));

export default function App() {
  const [flag, setFlag] = useState(true);
  const [count, setCount] = useState(0);

  const handleClick = async () => {
    // mimicing some async call
    // (ex, fecthing data from server, etc.)
    await sleep();

    setFlag(f => !f);
    setCount(c => c + 1);
  };

  useEffect(() => {
    // in this case, two console logs can be seen
    // since `setState` is called inside an asynchronous function
    // So,  React would not batch the updates, and perform two independent updates.
    console.log({ count, flag });

    // whenever `flag` or `count` changes, do somethig!
  }, [count, flag]);

  return (
    <>
      <h2>React's Batching Behavior while inside async callbacks</h2>;
      <p>Count: {count}</p>
      <button
        onClick={handleClick}
        style={{ backgroundColor: flag ? 'orange' : 'blue', color: '#fff' }}
      >
        Click me!
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Forced batching in async functions

To force setState to batch updates out of event handlers, unstable_batchedUpdates (an undocumented API) can be used:

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
});
Enter fullscreen mode Exit fullscreen mode

This is because React used to only batch updates during a browser event (like click), but here we're updating the state after the event has already been handled (in aync function):

For demo, see React 17: forced batching outside of event handlers

Opt out of automatic batching

Some code may depend on reading something from the DOM immediately after a state change. For those use cases, ReactDOM.flushSync can be used to opt out of batching

Continuing with our previous example,

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

  useEffect(() => {
    console.log({ count, flag });
  }, [count, flag]);

  const handleClick = () => {
    // setCount((c) => c + 1);

    // Force this state update to be synchronous.
    ReactDOM.flushSync(() => setCount(c => c + 1));
    // By this point, DOM is updated.

    setFlag(f => !f);
  };

  return (
    <div className='App'>
      <button onClick={handleClick}>Click Me!</button>
      <h3 style={{ color: flag ? 'blue' : 'black' }}>Count: {count}</h3>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

See ⚠️ ReactDOM.flushSync: Opt out of automatic batching in event handlers

  • However, ReactDOM.flushSync is not common & should be sparingly used.

flushSync flushes the entire tree and actually forces complete re-rendering for updates that happen inside of a call, so you should use it very sparingly. This way it doesn’t break the guarantee of internal consistency between props, state, and refs.

To read more about async behavior of this API & why setState is asynchronous, check out this awesome discussion RFClarification: why is setState asynchronous? #11527

Automatic Batching in React 18

React 18 includes some out-of-the-box improvements with ReactDOMClient.createRoot,
which includes support for automatic batching

Starting in React 18, all updates will be automatically batched, no matter where they originate from.

So, call to setState inside of event handlers, async functions, timeouts or any function will batch automatically (same as inside react events)
This will result in less rendering, and therefore better performance in react applications

function handleClick() {
  fetchSomething().then(() => {
    // React 18 and later DOES batch these:
    setCount(c => c + 1);
    setFlag(f => !f);
    // React will only re-render once at the end (that's batching!)
  });
}
Enter fullscreen mode Exit fullscreen mode
  • Note that this automatic batching behavior will only work in React 18 with ReactDOM.createRoot
  • React 18 with legacy ReactDOM.render keeps the old behavior
  • To read more about Automatic batching in React 18, see Automatic batching for fewer renders in React 18 #21

Want to read it on my blog ? Checkout this blog post

Top comments (1)

Collapse
 
shivamjjha profile image
Shivam Jha

While updating that state obj, you will do something like setState(prev => ({...prev, flag: !prev.flag})). That'a a single setter state call. That should do only re-render once. Batching is when multiple calls to state setter are made, and that component is rendered, only once