React.js, part 3: Suspense and useTransition — keeping the UI responsive while data loads
Part 3from the React.js series · 20 parts in all
Two concurrent features do most of the work in a modern React app: Suspense (which
declares what to show while something is pending) and useTransition (which
declares which updates are urgent and which can wait). This part shows both together, since
neither is much use alone.
Suspense: a boundary, not a data library
Suspense is a declaration: "if a child cannot render yet, show this fallback instead." The child throws a promise; the boundary catches it. Frameworks wire the fetching; you place the boundaries:
<Suspense fallback={<TableSkeleton />}>
<InvoiceTable customerId={id} />
</Suspense>
The important property is that the fallback replaces the boundary's whole contents, so nested boundaries let you stream a page in pieces: the shell renders immediately, and each slow section swaps in when it is ready.
useTransition: the search box that never stalls
Typing in a search field is urgent — the character must appear now. Filtering a
thousand-row table is not urgent — it can lag a frame. useTransition lets one
piece of state stay urgent while the derived expensive update is marked interruptible:
function Search() {
const [query, setQuery] = useState(''); // must feel instant
const [filter, setFilter] = useState(''); // may arrive late
const [isPending, startTransition] = useTransition();
function onChange(e) {
const next = e.target.value;
setQuery(next);
// The expensive list update is non-urgent; a newer keystroke can
// interrupt it and the pending one is thrown away.
startTransition(() => setFilter(next));
}
return (
<>
<input value={query} onChange={onChange} />
<span className={isPending ? 'dim' : ''}><BigList filter={filter} /></span>
</>
);
}
Rules that keep transitions honest
- A transition must be interruptible work, not a side effect. Never put an
API write inside
startTransition— only state updates that render. - The urgent update stays outside the callback. Set the input's own state first, then the deferred state inside it, or the input lags too.
- Suspense + a transition is the magic pair. A transition that suspends keeps the old UI visible (instead of a fallback flash) while the new data loads — the "stale-while-revalidate" feel, built in.
Next: React Server Components, where the boundary between "runs once on the server" and "runs in the browser" becomes the primary design decision.