More Than You Need to Know About ReactDOM.­flushSync

An in-depth look at what ReactDOM.flushSync does and what it’s good for.

Published · Last updated

flushSync is part of what I like to call “Rare React”; the curious & uncommon parts, the outer circle of the API, the rarely needed . That just so happens to be a thing that I enjoy diving into and writing a blog post about.

So what does ReactDOM.flushSync do and when is it useful?

The names suggest it flushes synchronously. But what is flushing, what is being flushed, and when should that happen synchronously? What is all that good for?

Let us turn to the docs first

flushSync lets you force React to flush any updates inside the provided callback synchronously

and

Call flushSync to force React to flush any pending work and update the DOM synchronously.

Okay, so now we know that it flushes state updates synchronously to the DOM. But what does synchronously mean here exactly? Why are state updates called pending work? And what does flushing even mean?

To answer these questions and to know what problem flushSync solves, we first need to know what React does on state updates.

What Does React Do On Updates?

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

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

return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>
{count}
</h1>
</div>
);
}
Update Queue
  1. This React event handler contains two state updates.

The Waiter Analogy for Updates

The React team often uses the following analogy to explain batching:

Updating your component’s state automatically queues a render. You can imagine these as a restaurant guest ordering tea, dessert, and all sorts of things after putting in their first order, depending on the state of their thirst or hunger.

[Batching] is great for performance because it avoids unnecessary re-renders. It also prevents your component from rendering “half-finished” states where only one state variable was updated, which may cause bugs. This might remind you of how a restaurant waiter doesn’t run to the kitchen when you choose the first dish, but waits for you to finish your order.

flushSync is like telling the waiter to run the kitchen right away with this specific order. Just like the waiter running to the kitchen for every individual order isn’t efficient (not to say rude), forcing every update to be applied immediately isn’t efficient.

The Other Sync Update: useSyncExternalStore

useSyncExternalStore is another de-opt that forces updates to be synchronous, it’s meant the keep the UI in sync with the state of external stores; third-party libraries or browser API’s.

Check out useSync­External­Store First Look and Concurrent React, External Stores, and Tearing

Time to answer the grand question: When is it useful?

Some browser APIs expect results inside of callbacks to be written to the DOM synchronously, by the end of the callback, so the browser can do something with the rendered DOM. In most cases, React handles this for you automatically. But in some cases it may be necessary to force a synchronous update.

Concretely, flushSync is for when you need to work around some issue where React updates the UI later than you need it to , requiring you to override batching.

  1. Code thats depends on reading something from the DOM immediately after a state change
    • Measuring a DOM Node
  2. Perform DOM side-effects immediately after a state change

Usually, batching is safe, but some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use ReactDOM.flushSync() to opt out of batching.

Ryan Florence, of Remix & React Router, recently tweeted about using using flushSync for what he calls transactional effects. This doesn’t seem like a common term, I also think it sounds arcane so I’m hesitant to use it myself.

Anyhow, it’s when you 1) update a state and 2) immediately call a DOM method (like .focus()) or set a property (.scroll) on an element ref, and only for certain events that update that state.

You could do this with use(Layout)Effect but that gets awkward because you only want to do perform the side effect for certain events in which that state is updated, not whenever the state is updated.

A little later he posted a video explaining it in more detail using a good, real-world example. I’ve quoted the essence of it here but it’s worth a watch.

useEffect and useLayoutEffect run immediately after [a component] mounts. This is the crux of the problem with focus management using useEffect and useLayoutEffect. They are synchronisation mechanisms.
React takes your state and synchronises it to the DOM. use(Layout)Effect takes your state and then you get to synchronise it somewhere else, maybe localStorage, wherever. […]

Focus is not — for me — a synchronisation task. It is a transactional effect, not a synchronisation effect. There are only specific transactions where I want to actually focus this [input element].

[...]

[With flushSync], I get to think: what happens in this event?

The code shown in the video can be viewed here: Trellix app/routes/board.$id/components.tsx.

Synchronisation effect
Do something when the component is added and when this state is updated
Transactional effect
After this particular state update in this particular action, do something immediately
Class Component this.setState Callback

this.setState in class components supports an optional callback that React will call after the update is commited . While class components still work, no-one writes them anymore. The post-update callback wasn’t carried over to the useState hook, so we’ve lost this functionality (kind of).

flushSync is similar in that it also lets you ‘do something’ (perform a side effect) right after a state update is commited, just in a different way.

this.setState callback


<button
onClick={() => {
this.setState({ edit: true }, () => {
ref.current.scrollTop = ref.current.scrollHeight;
});
}}
>

flushSync


<button
onClick={() => {
flushSync(() => {
setEdit(true);
});
ref.current.scrollTop = ref.current.scrollHeight
}}
>

this.setState callback

flushSync


<button
onClick={() => {
this.setState({ edit: true }, () => {
ref.current.scrollTop = ref.current.scrollHeight;
});
}}
>

Remix and React-Router (which Remix is built upon) recently introduced an option to do updates with flushSync to force internal react-router state updates to be synchronous. This is because react-router internal state updates are wrapped in startTransition to make them low-priority.

unstable_flushSync API
We've added a new unstable_flushSync option to the imperative APIs (useSubmit, useNavigate, fetcher.submit, fetcher.load) to let users opt-into synchronous DOM updates for pending/optimistic UI.

I must admit I haven’t played around with View Transitions yet but apparently flushSync is the way to make them work in React.

The key here is flushSync, which applies a set of state changes synchronously. Yes, there's a big warning about using that API, but Dan Abramov assures me it’s appropriate in this case. As usual with React and async code, when using the various promises returned by startViewTransition, take care that your code is running with the correct state.

  • Batching is a behind-the-scenes mechanism of React where it groups multiple state updates into a single re-render for better performance.
    • In this way, updates are async; they are not applied right away.
  • Flushing is when React applies changes that result from state updates.
  • flushSync opts out of batching, it forces a particular state update to be applied (flushed) right away (synchronously).
    • This is useful when React updates the UI later than you need it to, for example for managing focus and scroll positions after a state update.

There you have it, you now know more than you need to know about an obscure de-opt function of React. Remember that usually React does a fine job of scheduling updates, you need a good reason to override it.

// However, long term, we expect the main way you’ll add concurrency to your app is by using a concurrent-enabled library or framework. // In most cases, you won’t interact with concurrent APIs directly. // For example, instead of developers calling startTransition whenever they navigate to a new screen, router libraries will automatically wrap navigations in startTransition. // https://legacy.reactjs.org/blog/2022/03/29/react-v18.html

  1. React waits until all code in the event handlers has run before processing your state updates. This is why the re-render only happens after [multiple state updater] calls.

    This might remind you of a waiter taking an order at the restaurant. A waiter doesn’t run to the kitchen at the mention of your first dish! Instead, they let you finish your order, let you make changes to it, and even take orders from other people at the table.

    This lets you update multiple state variables — even [state variables] from multiple components—without triggering too many re-renders. But this also means that the UI won’t be updated until after your event handler, and any code in it, completes. This behavior, also known as batching, makes your React app run much faster. It also avoids dealing with confusing “half-finished” renders where only some of the variables have been updated.

    React does not batch across multiple intentional events like clicks — each click is handled separately. Rest assured that React only does batching when it’s generally safe to do. This ensures that, for example, if the first button click disables a form, the second click would not submit it again.

    React Docs: Queueing a Series of State Updates — React batches state updates
  2. When is setState asynchronous?
    [Pre React 18], setState is asynchronous inside event handlers. This ensures, for example, that if both Parent and Child call setState during a click event, Child isn’t re-rendered twice. Instead, React “flushes” the state updates at the end of the browser event. This results in significant performance improvements in larger apps. This is an implementation detail so avoid relying on it directly. [From React 18 onward], React will batch updates by default in more cases.
    React Legacy Docs: Component State
  3. React 18 adds out-of-the-box performance improvements by doing more batching by default. Batching is when React groups multiple state updates into a single re-render for better performance. Before React 18, we only batched updates inside React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default:

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

    This means that updates inside of timeouts, promises, native event handlers or any other event will batch the same way as updates inside of React events. This is a breaking change, but we expect this to result in less work rendering, and therefore better performance in your applications. To opt-out of automatic batching, you can use flushSync

    React Blog: React v18.0
  4. flushSync may 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.
    ReactDOM Docs: flushSync — Reference - Caveats
  5. callback: A function. React will immediately call this callback and flush any updates it contains synchronously. It may also flush any pending updates, or Effects, or updates inside of Effects. If an update suspends as a result of this flushSync call, the fallbacks may be re-shown.
    ReactDOM Docs: flushSync — Parameters
  6. [...] `flushSync first appeared in React 16 which was released over six years ago. we dropped the ball on documenting it but new docs had it documented the moment new docs went stable
    Tweet by Dan Abramov
  7. You also have an option to flush the entire tree if you know what you’re doing. The API is called ReactDOM.flushSync(fn). I don’t think we have documented it yet, but we definitely will do so at some point during the 16.x release cycle. Note that it actually forces complete re-rendering for updates that happen inside of the call, so you should use it very sparingly. This way it doesn’t break the guarantee of internal consistency between props, state, and refs.
    React GitHub — RFClarification: why is setState asynchronous?
  8. flushSync is the name of the method that lets control when React updates the screen ("flushes" the state updates). In particular, it lets you tell React to do it right now ("synchronously"): "Flush synchronously" = "Update the screen now".
    Normally, you shouldn't need flushSync. You would only use it if you need to work around some issue where React updates the screen later than you need it to. It is very rarely used in practice.
    React 18 Working Group: React Glossary + Explain Like I'm Five — flushSync
  9. optional callback: If specified, React will call the callback you’ve provided after the update is committed.
    React Legacy Docs: Component — setState Parameters