flushSync

Forces React to apply the state updates in a callback and update the DOM synchronously.

Signature
flushSync(callback)
  • Added in React 18.0.0

Parameters

ParameterTypeDescription
$callback() => voidA function React calls immediately, flushing any updates it contains synchronously. It may also flush pending updates, Effects, and updates made inside those Effects.

Return value

undefinedNothing. `flushSync` returns `undefined`.

Changelog

  • PHP 18.0.0Automatic batching was extended to promises, timeouts, and native event handlers, so updates that used to apply synchronously outside React events now need flushSync to do so.

React batches state updates and renders them later. flushSync suspends that behaviour for one callback: the updates inside it are rendered and committed to the DOM before the call returns, so the very next line can read the updated DOM.

When to reach for it

  • You must measure or scroll after a state change and cannot wait for an Effect — appending a chat message and immediately scrolling to the bottom, or focusing a field that only just rendered.
  • Non-React code is about to read the DOM: window.print(), a screenshot library, a third-party widget you hand a freshly rendered node to.
  • You are inside a native event handler or a browser callback where React's batching would otherwise defer the update past the point you need it.

What trips people up

It is a last resort, not a convenience. Every call forces a synchronous render of the whole pending update, which is exactly the work concurrent rendering exists to avoid, and calling it in a loop will make a list update visibly janky.

It can also flush work you did not ask for: pending updates queued from earlier, Effects and the updates they schedule, and suspended boundaries may drop back to their fallback. Calling it during render or from inside a lifecycle method does not work — React warns and the update is scheduled normally instead.

Before reaching for it, check whether useLayoutEffect, a ref callback, or a key change solves the problem without a forced flush.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Append a message to a chat log and scroll the container to the new bottom in the same handler.
  • Render a hidden print layout, then call window.print() once it is actually in the DOM.
  • Insert a new row and immediately focus its input without waiting for an Effect.
  • Update state inside a native addEventListener callback where React's batching would delay the DOM write.

Frequently asked questions

How do I make setState update the DOM immediately?

Wrap the update in flushSync(() => setState(next)) — React renders and commits before the call returns, so the next line can read the updated DOM. Use it sparingly; it forces a synchronous render.

When should I use flushSync instead of useLayoutEffect?

Use useLayoutEffect when you can measure after the commit React was already going to make, and flushSync only when you must force that commit early inside an event handler — for example before calling window.print() or scrollIntoView on a node that does not exist yet.

Why does flushSync make my Suspense fallback flash?

A synchronous flush cannot wait for suspended content, so any boundary with pending work drops back to its fallback. Move the update into a Transition, or flush only the state that must be applied immediately.

More examples

Scrolling to a row that was just added

Without flushSync, scrollerRef.current.lastElementChild would still be the previous message and the view would stop one row short. Flushing first guarantees the new list item is in the DOM before scrollIntoView measures it. Note that inline styles are used here only to make the container scrollable.

Example · jsx
import { useState, useRef } from "react";
import { flushSync } from "react-dom";

export default function ChatLog() {
  const [messages, setMessages] = useState(["Welcome."]);
  const scrollerRef = useRef(null);

  function send(text) {
    flushSync(() => {
      setMessages((list) => [...list, text]);
    });
    // The new <li> exists now, so lastElementChild is the message we added.
    scrollerRef.current.lastElementChild.scrollIntoView({ block: "end" });
  }

  return (
    <div>
      <ul ref={scrollerRef} style={{ maxHeight: 160, overflowY: "auto" }}>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
      <button onClick={() => send("Message " + (messages.length + 1))}>
        Send
      </button>
    </div>
  );
}

Rendering before handing the DOM to non-React code

window.print() snapshots the document as it exists at the moment of the call. Setting state normally would schedule the render after the handler returns, so the printed page would be missing the table. flushSync commits the table first, and the state is reset afterwards once printing has been dismissed.

Example · jsx
import { useState } from "react";
import { flushSync } from "react-dom";

function Invoice({ visible, lines }) {
  if (!visible) return null;
  return (
    <table>
      <thead>
        <tr>
          <th>Item</th>
          <th>Amount</th>
        </tr>
      </thead>
      <tbody>
        {lines.map((line) => (
          <tr key={line.item}>
            <td>{line.item}</td>
            <td>{line.amount}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default function PrintButton() {
  const [printing, setPrinting] = useState(false);
  const lines = [
    { item: "Hosting", amount: "40.00" },
    { item: "Domain", amount: "12.00" },
  ];

  function print() {
    flushSync(() => setPrinting(true));
    window.print(); // the table is in the document by now
    setPrinting(false);
  }

  return (
    <div>
      <button onClick={print}>Print invoice</button>
      <Invoice visible={printing} lines={lines} />
    </div>
  );
}

Updating from a native event listener

Since React 18 batches updates from native listeners too, the textContent read on the next line would show the previous width without the flush. With it, the paragraph's text matches the width that triggered the handler. This pattern is only worth it when you genuinely need to read back the rendered output; a plain Effect is cheaper.

Example · jsx
import { useState, useEffect } from "react";
import { flushSync } from "react-dom";

export default function ResizeReadout() {
  const [width, setWidth] = useState(() => document.documentElement.clientWidth);
  const [measuredAfter, setMeasuredAfter] = useState(null);

  useEffect(() => {
    function onResize() {
      const next = document.documentElement.clientWidth;
      flushSync(() => setWidth(next));
      // The readout element already shows the new number.
      setMeasuredAfter(document.getElementById("readout").textContent);
    }
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  return (
    <div>
      <p id="readout">Width: {width}</p>
      <p>Text read back after the flush: {measuredAfter ?? "not resized yet"}</p>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.