React / Performance and trustworthy boundaries / Lesson 13
Transitions and interruptible rendering
Distinguish urgent interaction from nonurgent rendering and reason about concurrent scheduling without assuming parallel JavaScript execution.
On this page
Lesson goals
- Concurrency changes scheduling, not domain truth
- Mark a secondary view update
- Respect version and async-action details
Concurrency changes scheduling, not domain truth
React can prepare rendering work with different priorities and interrupt or restart nonurgent work when more urgent input arrives. This does not turn a component’s JavaScript loop into a background worker or make side effects inside render safe. Keep render calculations pure so discarded work has no external consequence. A transition describes how an update should be scheduled, not whether the requested business operation is authorized or completed. Measure responsiveness under a representative workload rather than treating the word concurrent as a universal speed claim. The browser’s main thread still has finite time, and a single long synchronous function can block it.
Keep controlled input updates urgent
The state that directly controls a text input must update promptly from the event. Transition updates are not suitable for that controlling value. A separate selected view or costly result can be nonurgent while the input remains responsive. useTransition supplies a start function and pending state for owned transition updates; useDeferredValue can let a consumer lag behind an incoming value without owning its setter. Neither API is a fixed debounce timer. A deferred render may start quickly or remain behind depending on work, and it should be presented honestly if stale results remain visible.
Mark a secondary view update
The first example updates a selected local panel inside startTransition and displays pending feedback. Its two small panels are intentionally cheap, so pending may be too brief to observe; the example demonstrates API ownership rather than proving performance improvement. Tests assert the final selected content, not a minimum spinner duration or exact render count. The start callback runs immediately to schedule the relevant updates; it is not delayed execution of arbitrary code. Expensive preprocessing inside that callback still runs synchronously unless separately redesigned. Keep urgent input and direct user feedback outside the nonurgent state when required.
import { useState, useTransition } from "react";
export default function App() { const [tab, setTab] = useState("Overview"); const [pending, startTransition] = useTransition(); return <main><h1>Panels</h1><button type="button" onClick={() => startTransition(() => setTab("Details"))}>Show details</button>{pending && <p>Updating</p>}<h2>{tab}</h2></main>; }
After Show details settles: Details.
The selected view update is marked nonurgent. Its final value is deterministic, but the pending interval is not promised to be visible.
Defer a consumer while showing freshness
The second example keeps query text immediate and passes a deferred version into a pure filter. The interface explicitly says when results are catching up. This tiny dataset does not require optimization, but its relationships are visible and testable. Deferred rendering does not reduce network requests by itself or supply cache and cancellation rules. If the query drives remote loading, use the chosen data layer’s request identity and stale-result policy independently. A transition can coordinate a Suspense-enabled integration, but ordinary arbitrary promises created in render are not automatically a supported data source simply because a Suspense boundary surrounds them.
import { useDeferredValue, useState } from "react";
const names = ["Alpha", "Beta", "Gamma"];
export default function App() { const [query, setQuery] = useState(""); const deferred = useDeferredValue(query); const matches = names.filter(n => n.toLowerCase().includes(deferred.toLowerCase())); return <main><h1>Local search</h1><label>Query<input value={query} onChange={e => setQuery(e.target.value)} /></label>{query !== deferred && <p>Results catching up</p>}<ul>{matches.map(n => <li key={n}>{n}</li>)}</ul></main>; }
Typing bet eventually leaves only Beta in the results.
Typing is urgent, while the consumer may use an older query briefly. Stable names are valid keys for this fixed synthetic dataset.
Respect version and async-action details
React 19 supports richer async Actions around transitions than the shared synchronous examples used here. Current official guidance still documents important limitations, including marking state updates after await with an additional startTransition where required and managing out-of-order async completion. Do not copy a React 19 async-action example into a React 18 host and assume identical semantics. Frameworks may coordinate ordering for their own action APIs, while custom request flows remain responsible for it. Test rapid changes, interruption, stale presentation, error boundaries, and keyboard responsiveness. A pending indicator is scheduling feedback, not proof that a remote write was rolled back or succeeded exactly once.
Common errors and fixes
A controlled input lags under a Transition
- Why it happens
- Its controlling state was marked nonurgent.
- How to fix it
- Update the input directly and defer a separate consumer.
- Prevent it next time
- Test real typing under representative work.
startTransition still blocks on a long loop
- Why it happens
- The callback itself executes synchronously.
- How to fix it
- Reduce or move expensive work through an appropriate design.
- Prevent it next time
- Measure main-thread time separately.
A deferred query still sends many requests
- Why it happens
- Deferral was mistaken for network debouncing.
- How to fix it
- Design request admission and cancellation explicitly.
- Prevent it next time
- Separate render priority from transport policy.
Check your understanding
1. What should a test assert for a cheap transition example?
Show worked solution
Assert the final selected content and preserved urgent interaction. Do not require a spinner to remain visible for an invented duration, since a cheap render can complete before it is perceptible. Use a controlled representative workload when measuring the scheduling benefit itself.
2. Does useDeferredValue make a CPU-heavy filter execute on another thread?
Show worked solution
No. It coordinates rendering priority and permits stale consumer output while newer work is prepared. The calculation still runs in the relevant rendering environment. Optimize the algorithm or use an appropriate worker boundary when actual parallel computation is required.
Key takeaways
- Scheduling priority differs from parallel execution.
- Keep input control urgent.
- Async transition semantics are version-sensitive.