skip to content

In React 19, what does `flushSync` from `react-dom` do to the state updates queued inside its callback, and what is a legitimate reason to reach for it?

level: middleimportance: should knowfreq 46%

answer

  1. render and commit before the call returns
  2. still one pass, not one per setter
  3. only for imperative code that cannot wait
  4. scroll to a node that does not exist yet
  5. costs a blocking render, so keep it rare

basics

~20 s

flushSync(fn) runs fn and then forces React to render and commit the updates it queued to the DOM before flushSync returns, instead of batching them for later. You need it only when the next line of imperative code must see the updated DOM.

solid answer

~50 s

Normally a state setter queues an update and React commits it after the current work finishes. `flushSync(() => { setItems(next); })` overrides that: React renders and commits synchronously, so by the line after the call the DOM already reflects the new state. Updates inside the callback are still batched together into that one synchronous pass — `flushSync` does not mean one render per setter. The legitimate use is imperative interop where you cannot wait: appending an item and immediately scrolling or focusing the newly rendered node, updating the UI before calling `window.print()`, or handing control to a third-party library that reads the DOM straight after. It is deliberately narrow — it costs a blocking render, gives up React's ability to schedule the work, and React warns if you call it while it is already rendering, such as from inside a render or an effect that runs during commit.

code

jsx · 26 lines
jsx
import { useRef, useState } from 'react';
import { flushSync } from 'react-dom';

export default function Log() {
  const [lines, setLines] = useState([]);
  const listRef = useRef(null);

  function append() {
    flushSync(() => {
      setLines((prev) => [...prev, `line ${prev.length + 1}`]);
    });
    // the new node is committed, so this scrolls to it
    listRef.current.lastElementChild.scrollIntoView();
  }

  return (
    <>
      <button onClick={append}>append</button>
      <ul ref={listRef}>
        {lines.map((l) => (
          <li key={l}>{l}</li>
        ))}
      </ul>
    </>
  );
}

go deeper

for a junior

Know that flushSync comes from react-dom and forces React to update the DOM immediately instead of waiting, and that everyday components should not need it.

for a middle

Explain the exact semantics — one synchronous render and commit before the call returns, with the callback's updates still batched together — and give a concrete imperative case that requires it.

for a senior

Demonstrate judgment about cost: it blocks, it breaks batching with surrounding work, and it removes React's scheduling freedom, so you scope it to the single call that needs it and reject it as a general fix.

for a principal

Treat every flushSync in a codebase as a documented exception with an owner and a reason, since each one pins behavior to synchronous commit and constrains later adoption of concurrent scheduling.

## The default, and what flushSync changes By default a state setter is a request: React queues the update and performs the render and DOM commit after the currently running work finishes, before the browser paints. That is what makes batching possible, and it means that on the line immediately after a setter, the DOM still shows the old output. `flushSync` from `react-dom` suspends that arrangement for one callback: ```jsx import { flushSync } from 'react-dom'; flushSync(() => { setItems([...items, newItem]); }); // the new <li> exists in the DOM right here listRef.current.lastElementChild.scrollIntoView(); ``` React runs the callback, then immediately renders and commits everything the callback queued, and only then returns. Two details people get wrong: - **Updates inside the callback are still batched.** `flushSync(() => { setA(1); setB(2); })` performs **one** synchronous render with both applied. `flushSync` changes *when* the flush happens, not how many flushes there are. - **Updates queued outside the callback get flushed too.** If work was already pending when you call it, that pending work is flushed as part of making the DOM consistent. `flushSync` is not a surgical instrument that touches only your two setters. ## When it is actually justified The honest rule: only when a piece of imperative code that runs *right now* must observe the committed DOM, and there is no rendering-based way to express the same thing. - **Act on a node that does not exist yet.** You add a row, a message, or a tab and then must scroll it into view or move focus to it. The node is only in the DOM after a commit. - **Hand off to a browser API that reads the document synchronously.** `window.print()` is the classic case: whatever is committed at the moment you call it is what gets printed. - **Interop with a non-React library** that you must call immediately after changing React-owned markup, where the library inspects the DOM on the spot. - **Some native event handlers**, where a browser default behavior depends on the document state before the event finishes. Everything else — "my state feels laggy", "I want the update to happen now", "the value did not change on the next line" — is not a reason. The last one in particular is a misdiagnosis: the state variable in the current scope is a per-render value and `flushSync` does not change it, so reaching for `flushSync` to "read the new state" fixes nothing. ## What it costs A `flushSync` call is a blocking render plus a DOM commit inside whatever code path called it. During it the browser can do nothing else. Concretely you give up: - **Batching with surrounding work.** Updates that would have merged into one pass with the rest of the interaction now flush separately, so you can end up with *more* total render passes, not fewer. - **Scheduling flexibility.** React cannot defer, interleave, or drop the work; it happens now, at whatever priority the rest of the frame needed. - **Cheap repetition.** Inside a loop or a high-frequency handler, each call is a full render and commit, which is how flushSync turns a smooth interaction into a stuttering one. React also warns when `flushSync` is called while React is already rendering — for example from inside a component body or from an effect that runs during the commit — because it cannot flush re-entrantly. If you see that warning, the answer is to restructure, not to silence it. ## How to answer State the semantics in one sentence ("renders and commits the callback's updates before returning"), correct the two common misreadings (still batched inside; also flushes pending work), give one concrete legitimate case, and name the cost. Signalling that it is an escape hatch you use rarely is a bigger part of the answer than the mechanics.

  • Does flushSync(() => { setA(1); setB(2); }) cause one render or two?
    One. The updates queued inside the callback are still batched together; `flushSync` only changes when that single pass happens — synchronously, before the call returns, instead of after the surrounding work finishes. Two calls to `flushSync`, one per setter, would give you two synchronous renders, which is the version that actually hurts.
  • A developer uses flushSync because the state variable still holds the old value on the next line. Is that the right tool?
    No — it does not help. The state variable in scope belongs to the current render and keeps its value for that render's whole lifetime, whether or not React has committed a new one. `flushSync` makes the *DOM* current, not the local binding. If they need the new value in that scope, they should compute it into a local variable and pass it to the setter.
  • Why does React warn when flushSync is called from inside a component render or during a commit?
    Because React is already in the middle of rendering and cannot start a nested synchronous flush safely. The warning marks a structural mistake: rendering must stay a pure function of props and state, and commit-time work should schedule an update rather than force one. The fix is to move the call into an event handler or a passive effect, not to suppress the warning.

saying these in an interview costs you the question

  • Uses flushSync so the state variable updates on the next line
  • Thinks flushSync makes each setter render separately
  • Treats flushSync as a performance optimization
  • Calls flushSync inside a render or a loop over items
  • Believes it only flushes the updates inside its own callback

context