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?
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>;}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
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

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?
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>;}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?
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} /> </> );}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?
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> </> );}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?
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>;}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?
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} /> </> );}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"?
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> </> );}Go deeper
Pro tip
- Test concurrent rendering with React's own mock scheduler, not with timers. Load
scheduler/unstable_mockin place ofscheduler, 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, withreact-actions-formsstep 4 for a form Action before its firstawait) - 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.
Related lessons
- Read first: useState: hook slots, snapshots and batching
- Read first: Interaction to Next Paint
- Related: Render and commit
- Related: Memoization and React Compiler
- Related: Suspense and error boundaries: what waits and what fails together
- Related: Context vs external stores
- Related: Forms, Actions and optimistic UI
- Related: Debounce vs throttle
Checked against the tools and docs as of 2026-10-09.