Concurrent Rendering in React: How It Works and Why It Matters

Rendering & Frameworks

Traditional React — often called "legacy mode" — renders synchronously. When state changes, React starts rendering and doesn't stop until the whole tree is updated. Concurrent rendering changes that fundamental contract: React can start a render, interrupt it if something more urgent arrives, and resume later. Multiple renders can be "in progress" at different priority levels simultaneously.

This isn't automatic. You must opt in to the concurrent renderer by using createRoot (available since React 18), and then explicitly mark work as low-priority to take full advantage.

The Core Idea: Rendering Is No Longer Instantaneous

In legacy React, a state update triggers a synchronous render — the component tree updates in one pass before the browser gets a chance to repaint. If that render takes 200ms (slow device, large tree), the UI freezes for 200ms.

Concurrent rendering introduces the concept of render lanes and a scheduler that can yield to the browser when a frame deadline approaches:

[User types]
  → React starts rendering search results (low priority)
  → Frame deadline approaches
  → React yields — browser paints, user sees keypress immediately
  → React resumes search results render
  → Eventually commits search results to DOM

The user sees smooth input response. The search results update slightly later. Both are correct and visible — just on different timelines.

The APIs That Expose Concurrency

useTransition

useTransition marks a state update as non-urgent. React can interrupt it if higher-priority updates arrive:

const [isPending, startTransition] = useTransition()

function handleSearch(query) {
  // Urgent: update input immediately
  setQuery(query)

  // Non-urgent: re-render results, can be interrupted
  startTransition(() => {
    setResults(search(query))
  })
}

return (
  <>
    <input value={query} onChange={e => handleSearch(e.target.value)} />
    {isPending && <Spinner />}
    <ResultsList results={results} />
  </>
)

When the user types quickly, each new keystroke interrupts the previous in-progress search render. React discards the partial render and starts fresh with the latest query. The input always responds instantly.

useDeferredValue

useDeferredValue is the hook equivalent for when you don't control the state update — for example, when a value comes from a parent prop:

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query)

  // This render may use a stale deferredQuery — that's ok
  // It will update when the concurrent render for the
  // new value completes
  return <ExpensiveList query={deferredQuery} />
}

The deferredQuery lags behind query — React first renders with the old value (fast, already computed), then renders with the new value at low priority in the background.

Tearing and Why Concurrent Rendering Makes It Possible

In a synchronous render, a single snapshot of state is used throughout the entire render pass. Concurrent renders can be interleaved, which means state can change while a render is in progress. If different parts of the tree read the same external store at different render times, they might see different values — this is called tearing.

React's useSyncExternalStore hook solves this for external stores by forcing synchronous reads during concurrent renders:

const value = useSyncExternalStore(
  store.subscribe,
  store.getSnapshot,    // client
  store.getServerSnapshot, // server
)

This is why state management libraries had to update their React integrations for concurrent mode — naive useEffect-based subscriptions are susceptible to tearing.

Concurrent Features That Depend on This

  • Suspense for data fetching — React can "pause" a tree that's waiting for data without blocking other visible UI
  • Streaming SSR — the server can send HTML in chunks, and React can hydrate them at different priorities
  • Offscreen rendering — React can pre-render hidden components (e.g. a tab not currently visible) at idle priority

All of these require the ability to have multiple renders at different stages simultaneously — which requires Fiber's work loop.

What Doesn't Change

Concurrent rendering doesn't mean components render in parallel threads. JavaScript is still single-threaded. Concurrency here means interleaving: React pauses a low-priority render to run a high-priority one on the same thread.

Synchronous state updates inside event handlers are still synchronous in React 18's "automatic batching" model. Calls to setState inside native DOM event handlers, setTimeout, and Promise callbacks are now batched automatically, reducing unnecessary renders without changing the concurrency model.

The Mental Model Shift

In legacy React, you could reason: "when I call setState, React will render before the next user interaction." In concurrent React, that guarantee is gone for low-priority updates.

This matters for:

  • Reading DOM layout immediately after a state update (use effects, not inline code)
  • Code that assumes renders happen in order (rare, but it exists)
  • Any library that hooks into the render lifecycle without using the concurrent-safe APIs

For most application code, useTransition and useDeferredValue are the only two places where you actively manage concurrency. Everything else works the same as before.

Conclusion

Concurrent rendering lets React prioritise urgent updates (user input, clicks) over expensive background work (large list renders, data-driven re-renders). The mechanism is React's Fiber architecture yielding to the browser scheduler between units of work. The developer APIs are useTransition for marking work as low-priority and useDeferredValue for deferring derived values. The result is UIs that stay responsive even during heavy rendering — without threads, workers, or explicit async code.