Skip to content

Transitions and concurrent rendering

A Transition lets urgent input commit first and redoes stale work; it moves rendering out of the way and never makes a render faster.

Predict first

Before you read: predict

ExamHall's admin results search lists 50,000 results in 40 exam sessions. This draft:

The admin types o. React has rendered 12 of the 40 session groups for "o" when she types k. She then stops, and React finishes all its work.

Each session group shows the count of its rows matching the query.

Which results has the table shown, in order?

Code for this question
function ResultsSearch({ sessions }) {  const [text, setText] = useState('');  const [query, setQuery] = useState('');  const [isPending, startTransition] = useTransition();  return (    <>      <input value={text} onChange={(e) => {        setText(e.target.value);        startTransition(() => setQuery(e.target.value));      }} />      {isPending && <span>Updating…</span>}      <ResultsTable sessions={sessions} query={query} />    </>  );} const ResultsTable = memo(function ResultsTable({ sessions, query }) {  return sessions.map((s) => <SessionGroup key={s.id} session={s} query={query} />);}); function SessionGroup({ session, query }) {  const hits = session.rows.filter((r) => r.name.toLowerCase().includes(query));  return <p>{session.title}: {hits.length} rows</p>;}
Pick one

The 30-second version

Updates set in startTransition, or in a form Action before its first await, are Transitions; others are urgent. React commits urgent ones first: the box shows the key, the old results stay, isPending is true. React renders a Transition and throws a half-done one away when urgent input arrives, restarting with the newest state. It never makes a render faster.

A Transition lets urgent input commit first and redoes stale work; it moves rendering out of the way and never makes a render faster.

How it works

Step 1 of 10

Two kinds. A set call during startTransition's function, or a form Action before any await (react-actions-forms step 4), marks a Transition; others are urgent.

Analogy: A newspaper cartoonist drawing tomorrow's strip

She works alone at one board, drawing tomorrow's strip panel by panel, while the editor phones with quick questions.

At the board In React
she answers the editor's call between panels an urgent update commits first (step 2)
today's paper keeps the old strip; a note says "new strip coming" the old results stay; isPending (steps 2–3)
she never stops in the middle of a panel React yields between components, never inside one (step 4)
the news changes: the half-drawn strip goes in the bin, and she starts again from panel one a half-done render is thrown away and redone with the newest state (step 5)
answering calls never draws the strip faster not faster (step 6)

Breaks down when: she keeps the panels the news did not touch; React discards the whole unfinished render. Wrong inference: "React resumes where it stopped".

Contrasting case: a debounce waits for the input to go quiet before any work starts (js-debounce-throttle); a Transition starts at once and is redone only when overtaken.

In real terms: a Transition is a lower-priority render that React may abandon and redo; it keeps urgent commits flowing, running the same code on the same thread.

Examples, run for real

4.1 ExamHall: EXH-3320 and MR !3620

The pilot kept one query state for the box and the table, and ResultsTable was a plain component: every key re-rendered all 40 session groups before the box could show it. MR !3620 keeps text urgent, sets query in a Transition and wraps ResultsTable in memo (steps 1–3, 8):

// EXH-3320, the pilot
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultsTable sessions={sessions} query={query} />

// MR !3620
<input value={text} onChange={(e) => {
  setText(e.target.value);
  startTransition(() => setQuery(e.target.value));
}} />
{isPending && <span>Updating…</span>}
<ResultsTable sessions={sessions} query={query} />   // ResultsTable = memo(…)
4.1 EXH-3320 pilot (one state): key "z" · 1 commit: box "z", table 6,800 rows · SessionGroups rendered before the box shows "z": 40
4.1 MR !3620 (Transition): key "z" · commit 1: box "z", table all 50,000 rows, "Updating…" shown · SessionGroups rendered before it: 0
4.1 MR !3620 (Transition): commit 2: table 6,800 rows, "Updating…" gone · SessionGroups rendered for it: 40
4.1 SessionGroups rendered per key: pilot 40, MR 40

The box no longer waits for the table. For one key, the table still renders all 40 groups: the work moved, it did not shrink (step 6).

4.2 Media streaming: a podcast episode search with a deferred value

EpisodeList gets the box's text as query and defers it itself; only the grid inside it, wrapped in memo, reads the deferred value (steps 7–8):

<input value={text} onChange={(e) => setText(e.target.value)} />
<EpisodeList shows={shows} query={text} />

// inside EpisodeList({ shows, query })
const deferredQuery = useDeferredValue(query);
<h2>Episodes for "{query}"</h2>
<ShowGrid shows={shows} query={deferredQuery} />   // ShowGrid = memo(…)
4.2 40 shows · key "n" · render 1 (urgent): box "n", heading "n", grid for "" · ShowRows rendered: 0
4.2 render 2 (background): heading "n", grid for "n" · ShowRows rendered: 40

In the urgent render one component holds two values: the heading already says "n" while the grid still gets "", so memo skips it. The background render draws the grid with the new text.

Picture it

a woman in a blue suit, seated at a desk, takes a small green card from a man in a green jacket standing in front of her. On a separate side desk beside her lies a large sheet with a plan drawn on it, half rolled up and held by a brown band. No text. Read it as: the card is the urgent update she handles now; the half-rolled plan is the Transition, set aside unfinished.
An illustration to help it stick. The mechanism is in the sections above.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 8

A video service's title search, over 40 shows:

A viewer types j in the empty box.

In the render that puts j in the box, how many ShowRows render, and with which text?

Code for this question
function TitleSearch({ shows }) {  const [text, setText] = useState('');  return (    <>      <input value={text} onChange={(e) => setText(e.target.value)} />      <ShowList shows={shows} query={text} />    </>  );} function ShowList({ shows, query }) {  const deferredQuery = useDeferredValue(query);  return shows.map((s) => <ShowRow key={s.id} show={s} query={deferredQuery} />);} function ShowRow({ show, query }) {  const hits = show.titles.filter((t) => t.toLowerCase().includes(query)).length;  return <p>{show.name}: {hits} titles</p>;}
Pick one

Question 2 of 8

A ticket site's seat search; SeatMap lists the seats whose label contains filter:

A fan types row quickly: each key lands before React has rendered the Transition of the key before it. She stops, and React finishes all its work.

What does the box show?

Code for this question
function SeatSearch({ seats }) {  const [text, setText] = useState('');  const [isPending, startTransition] = useTransition();  return (    <>      <input value={text} onChange={(e) => startTransition(() => setText(e.target.value))} />      {isPending && <span>Searching…</span>}      <SeatMap seats={seats} filter={text} />    </>  );}
Pick one

Question 3 of 8

A payments ledger's search; countMatches checks every one of the ledger's 50,000 entries:

A clerk types 7.

In what order do these happen?

Code for this question
function LedgerSearch({ entries }) {  const [text, setText] = useState('');  const [count, setCount] = useState(entries.length);  const [, startTransition] = useTransition();  function onChange(e) {    setText(e.target.value);    startTransition(() => setCount(countMatches(entries, e.target.value)));  }  return (    <>      <input value={text} onChange={onChange} />      <p>{count} entries</p>    </>  );}
Pick one

Question 4 of 8

A chat app's history search, over 30 conversations; countHits(c, query) searches one conversation:

A user types s. React has rendered 10 of the 30 Hits for "s" when she types h. She then stops, and React finishes all its work.

How many times has countHits run since she typed s?

Code for this question
function ChatSearch({ conversations }) {  const [text, setText] = useState('');  const [query, setQuery] = useState('');  const [, startTransition] = useTransition();  return (    <>      <input value={text} onChange={(e) => {        setText(e.target.value);        startTransition(() => setQuery(e.target.value));      }} />      <History conversations={conversations} query={query} />    </>  );} const History = memo(function History({ conversations, query }) {  return <ul>{conversations.map((c) => <Hit key={c.id} c={c} query={query} />)}</ul>;}); function Hit({ c, query }) {  return <li>{c.title}: {countHits(c, query)} hits</li>;}
Pick one

Question 5 of 8

p5-start.jsx is ExamHall's proctor roster search as the pilot shipped it: one query state drives both the box and RosterTable, which renders 40 SessionGroups. The tests: (T1) the box shows each key in a commit that renders no SessionGroup; (T2) three keys typed before React renders keep all three letters; (T3) the table ends on the last text. Keep the names ProctorRoster({ sessions }) and SessionGroup.

Change p5-start.jsx so that it passes T1, T2 and T3.

Question 6 of 8

An interviewer says: "Filtering the 50,000-row results table makes the box lag; tell me how useTransition helps, and when it would not".

Write three sentences.

Question 7 of 8

A home-goods shop's product search; ProductGrid lists the products whose name contains query, and it is wrapped in memo:

Shoppers lose letters: typing lamp quickly, each key before React renders the one before, leaves p in the box. The fix calls setText(e.target.value) directly and adds const deferredText = useDeferredValue(text);.

Which props make the box show every key as it is typed and end on "lamp", the grid end on the lamps, and no commit that puts a key in the box render the grid?

Code for this question
function ProductSearch({ products }) {  const [text, setText] = useState('');  const [, startTransition] = useTransition();  return (    <>      <input value={text} onChange={(e) => startTransition(() => setText(e.target.value))} />      <ProductGrid products={products} query={text} />    </>  );}
Pick one

Question 8 of 8

A music app's track search. The list should dim while it is out of date:

TrackList is wrapped in memo. The list shows "jaz", and the listener types z.

Which expression in the slot dims the list in the commit that shows "jazz" in the box, and undims it once the list shows "jazz"?

Code for this question
function TrackSearch({ tracks }) {  const [query, setQuery] = useState('jaz');  const deferredQuery = useDeferredValue(query, '');  const [isPending] = useTransition();  const dim = ____;  return (    <>      <input value={query} onChange={(e) => setQuery(e.target.value)} />      <div style={{ opacity: dim ? 0.5 : 1 }}>        <TrackList tracks={tracks} query={deferredQuery} />      </div>    </>  );}
Pick one

Go deeper

Pro tip
  1. Test concurrent rendering with React's own mock scheduler, not with timers. Load scheduler/unstable_mock in place of scheduler, then stop React after a chosen component and dispatch the key there. When: checking what an interruption does (steps 4–5) without depending on machine speed. Status: RAN (React 19.3.0, scheduler 0.28.0, jsdom 30.1.1). The mock build is an internal test tool, not a public API.
Sources

They support the teaching; no key is cited. Valid as of 2026-10-09 (React 19.3).

  • react.dev, useTransition: https://react.dev/reference/react/useTransition (react-usetransition.txt). Quoted: "The function you pass to startTransition is called immediately, marking all state updates that happen while it executes as Transitions." (step 1) "A state update marked as a Transition will be interrupted by other state updates." (steps 4–5) "Transition updates can’t be used to control text inputs." (step 9) "The isPending flag that tells you whether there is a pending Transition." (step 3) "React calls action immediately with no parameters and marks all state updates scheduled synchronously during the action function call as Transitions." (step 1, with react-actions-forms step 4 for a form Action before its first await)
  • react.dev, useDeferredValue: https://react.dev/reference/react/useDeferredValue (react-usedeferredvalue.txt). Quoted: "During updates, React will first attempt a re-render with the old value (so it will return the old value), and then try another re-render in the background with the new value (so it will return the updated value)." (step 7) "This optimization requires SlowList to be wrapped in memo" (step 8) "The background re-render is interruptible" (step 5)
  • react.dev, <Activity>: https://react.dev/reference/react/Activity, the row's third source, fetched for the scope decision only; nothing here teaches it.

Checked against the tools and docs as of 2026-10-09.