skip to content

In a React 19 app, a large list is mid-render inside a transition when the user types another character into the search input. React shows the new character immediately and updates the list afterwards. What does React do internally so that one pending update jumps ahead of another?

level: middleimportance: must knowfreq 58%

answer

  1. two pending updates, two priorities
  2. priority is stamped when setState is called
  3. urgent work is selected first
  4. invalidated work is thrown away, not resumed

basics

~20 s

React stamps each update with a priority lane when the setter is called: a keystroke gets an urgent lane, a transition a low one. The scheduler renders the highest pending priority first, discards the unfinished transition render, and restarts it afterwards.

solid answer

~50 s

Every update carries a priority that React assigns at the moment `setState` is called, based on the context that scheduled it. A setter called synchronously from a discrete event like `keydown` gets an urgent priority; one called inside `startTransition` gets a transition priority that explicitly means "this may be delayed". Because a transition render is time sliced, React yields to the browser periodically, and that gap is when the keystroke can actually arrive. When React resumes and sees urgent work pending on the same root, it does not continue the half-built tree — it discards that work, renders and commits the urgent update alone against the current state, and then schedules the transition again from scratch with the newest query. Nothing from the abandoned render reaches the DOM, so the user only sees the input respond instantly and the list catch up.

code

jsx · 26 lines
jsx
import { useState, useTransition } from 'react';

export function Search({ items, filter }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleChange(event) {
    const next = event.target.value;
    setQuery(next); // urgent: the input must show the keystroke now
    startTransition(() => {
      setResults(filter(items, next)); // low priority: interruptible, restartable
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      <ul style={{ opacity: isPending ? 0.6 : 1 }}>
        {results.map((item) => (
          <li key={item.id}>{item.label}</li>
        ))}
      </ul>
    </>
  );
}

go deeper

for a junior

Recall that updates from typing or clicking are treated as urgent while updates wrapped in a transition are explicitly allowed to be delayed, and that React renders the urgent one first.

for a middle

Explain the mechanics: the lane is stamped when the setter is called, the scheduler picks the highest pending priority, and an invalidated in-progress render is discarded and restarted rather than resumed.

for a senior

Demonstrate the operational consequences — repeated preemption redoes work, sustained input can keep a transition from committing, and render bodies must be pure because they may run several times per visible update.

for a principal

Own the tradeoff: prioritisation reorders work but never reduces it, so decide deliberately which interactions get the urgent budget and when the honest answer is to shrink the render instead of reprioritising it.

## Two updates, two priorities React does not render "an update"; it renders a root whose pending work is a set of scheduled updates. Calling a state setter does not render anything immediately. React records the update on that component's queue and stamps it with a priority — internally called a **lane** — chosen from the context in which the setter ran: - a setter called synchronously inside a discrete user event (`click`, `keydown`, `input`) gets an **urgent** priority, because the user is watching for exactly that feedback; - a setter called inside the callback passed to `startTransition`, or the one returned by `useTransition`, gets a **transition** priority, which is a declaration that the result may be delayed; - anything else — a setter from a timer, a promise callback, an effect — lands at the default priority in between. React then asks its scheduler to do work on that root. The scheduler always selects the highest-priority pending lanes to render, and leaves everything lower still marked as pending. ## Where the interruption window comes from Under a fully synchronous render there is no window at all: React starts rendering and does not return to the browser until it has committed, so no event can be delivered "during" the render. Concurrent rendering changes that. A transition-priority render is **time sliced**: React renders some components, notices it has used its slice of time, stops, and schedules the continuation as a fresh browser task. In that gap the browser paints and dispatches the pending keystroke, React's event system runs the input's urgent `setState`, and an urgent lane appears on the same root. ## Preemption discards; a plain yield resumes React builds its work into a *work-in-progress* tree kept separate from the committed tree. When the scheduler wakes up and sees urgent work pending, it does not keep building the transition tree — that tree was computed from state the urgent update has just invalidated. It throws the partial work away and starts a fresh render at the urgent priority from the current committed state, including the new query value. That render is small (only the input's subtree changed), so it commits almost immediately and the character appears. The asymmetry is what candidates miss: - yielding because the time slice ran out **resumes** where it stopped — nothing invalidated the work; - being preempted by a higher-priority update **restarts** — the basis of the render changed. Both feel like "React paused", but only one keeps the partial work. After the urgent commit, the transition lane is still marked pending, so React schedules it again and re-renders the list against the newest query. If another keystroke lands first, the cycle repeats — which is why sustained fast typing can keep a transition from committing until the user pauses, and why `isPending` stays true across that whole period. ## Why the user never sees a broken tree Nothing from an abandoned render is written to the DOM. The render phase only calls component functions and builds fibers; the commit phase — DOM mutation, layout effects, then passive effects — is synchronous and is not interrupted. Intermediate states therefore exist only inside React's memory. The price you pay is that a component function may run several times for one visible update, which is why render bodies must be pure: a side effect written there happens once per *attempt*, not once per commit. ## The everyday consequence This is the mechanism behind "the input stays responsive while the list catches up". The list still costs exactly as much CPU as before — arguably more, since restarted renders repeat work. What changed is *ordering*: React was given enough information to know which of two pending updates the user is waiting on. Without a priority signal, React has no way to distinguish them and renders them together in one batch, so the keystroke waits for the list. ## Interview traps Two claims are wrong and are commonly offered. First, "the transition render pauses and resumes from where it was" — true for a slice yield, false for preemption. Second, "React commits the partial list so it can show the keystroke" — React never commits a partly-rendered tree; a partial commit would show an inconsistent UI. If you are asked to force the opposite behaviour, `flushSync` from `react-dom` is the escape hatch that makes an update render synchronously, at the cost of the responsiveness you were buying.

  • Both setters are called in the same event handler — why do they not end up in one batch at one priority?
    Batching groups updates that will be rendered together, but each update still carries its own lane. The `setQuery` call is stamped urgent from the discrete event, while the one inside `startTransition` is stamped transition. React renders the urgent lane first and leaves the transition lane pending, so batching and prioritisation are separate mechanisms.
  • What happens if the user keeps typing quickly and never pauses?
    Each keystroke schedules urgent work that preempts the in-flight transition render, so the low-priority render keeps restarting from the newest query and may not commit until typing slows. `isPending` stays true throughout, which is the intended signal for showing a stale-results affordance. The wasted restarts are real CPU, which is why the underlying render still needs to be cheap.
  • Can you force an update to skip all of this and render synchronously?
    Yes — `flushSync` from `react-dom` renders and commits the wrapped update before returning, which is occasionally needed when you must measure the DOM or coordinate with a non-React library immediately afterwards. It gives up interruptibility for that update, so it blocks the frame like a legacy synchronous render and should be rare and deliberate.

saying these in an interview costs you the question

  • Says the interrupted render resumes from the exact fiber it stopped at
  • Claims React commits the partial list so the keystroke can show
  • Thinks priority is decided at render time from how slow a component is
  • Believes the transition ends up costing less total CPU

context