React flushSync()

The ReactDOM.flushSync(callback) API method synchronously flushes all the updates inside the callback passed, into the DOM immediately. Let’s break this sentence down to understand it better.

  • “Flushes all the updates inside the callback passed” – A component is updated when its state changes. So any setState() call (instance method for Class component and Hooks API for Functional component) inside the callback argument will be flushed.
  • “Flushes synchronously into the DOM immediately” – Flushing into the DOM simply means the components associated with the set state calls will be re-rendered immediately. The word synchronously is used because otherwise, set state calls are asynchronous.

Now let’s understand flushSync() practically by going through an example. Say we had a component like this:

import { useState } from 'react';

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

  // A click event handler
  function handleClick() {
    setCount(c => c + 1);

    // ... after code
  }

  return (
    <button onClick={handleClick}>Set Count</button>
    <p>Count: {count}</p>
  );
}

When the button is clicked, the setCount (inside handleClick) call will run asynchronously, that’s just how React works. This means that the code following setCount above (denoted by ...after code), will be executed before the count state is actually updated. Due to this asynchronous execution, if we try to access the updated count flag right after updating it, we will end up with the previous state value and not the new one.

setCount(c => c + 1);
console.log(count); // On first click: logs 0, not 1!

This behaviour kind of makes sense. To access the updated count value, React would have to re-render the component after the setCount call. Imagine a component with N number of set state calls followed by code that reads the updated state values. That will lead to a lot of re-renders (across component hierarchies) just to access the updated values.

If we really want to get the updated value, we can pre-compute the new state and store it in a variable first and then use that.

const [count, setCount] = useCount(5);
const updatedCount = count + 1;
setCount(updatedCount);
console.log(updatedCount); // Read the updated value

// or if updating state with the callback method

setCount((count) => {
  const updatedCount = count + 1;
  console.log(updatedCount); // Read the updated value

  return updatedCount;
});

This works just fine. But coming to the main point of this article, what if instead of the updated state value, we wanted to access some information immediately from the updated tree itself? What I mean is once a component’s state is updated, it re-renders and updates a bunch of other things – props, refs and most importantly the DOM. What if we wanted to access some updated value from one of these?

// A click event handler
const handleClick = () => {
  // (1) Set a state
  setSomeState(...);
  // (2) Access something from the DOM
  // (3) ... Perform some computation and operation based on (2)
};

ReactDOM.flushSync() lets us solve this exact problem by doing this:

import { flushSync } from 'react-dom';

function Component(props) {
  const [someState, setSomeState] = useState();

  // ... more code

  const handleClick = () => {
    // 1. Run the code passed in the callback
    // 2. Batch execute all set state calls inside the callback synchronously
    // 3. Re-render the component (flush changes to DOM)
    flushSync(() => {
      setSomeState(...);
    });

    // We can access the updated props, refs or the DOM here
    // Thanks to flushSync()!

    // If we updated another state value here,
    // that'd cause another re-render
  };

  return (...);
}

flushSync() executes all the code passed in the callback and if it finds any set state calls, then it’ll batch execute them together. React will ensure that the states are updated synchronously before proceeding to the code right after the flushSync() call itself. A synchronous update of the states will cause the component to re-render and the code following flushSync() can then access the updated props, refs and the web page’s DOM.

Flushing the entire tree is a way to opt out of automatic batching as well. In react when a code path makes multiple set state calls within a single component or across component hierarchy:

setA(...);
setB(...);
setC(...);
setD(...);

The calls are batched together for execution to avoid re-rendering components after each call.

setA(...);
// No render
setB(...);
// No render
setC(...);
// No render
setD(...);
// Yes render!

But there are cases like toggling a state value or accessing the DOM immediately after updating a state (as we discussed above), where we have to break out of automatic batching. flushSync() allows us to do this (kind of repeating the example code that we saw above):

flushSync(() => { setA(...); });
// Yes render!
flushSync(() => { setB(...); });
// Yes render!
flushSync(() => { setC(...); });
// Yes render!
flushSync(() => { setD(...); });
// Yes render!

Some people resort to weird hacks to avoid automatic batching:

setA(...);

setTimeout(() => {
  setB(...);
}, 0);

Don’t do that, use flushSync(), but carefully knowing the fact that it forces complete re-rendering of components for updates happening inside of the callback, significantly hurting performance.

Some other points to keep in mind (from the docs):

flushSync may force pending Suspense boundaries to show their fallback state.

flushSync may also run pending effects and synchronously apply any updates they contain before returning.

flushSync may also flush updates outside the callback when necessary to flush the updates inside the callback. For example, if there are pending updates from a click, React may flush those before flushing the updates inside the callback.

Leave a Reply

Your email address will not be published.