Trust is earned, not given

A different perspective

2022-12-06 · Projects

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

  1. A transition must be interruptible work, not a side effect. Never put an API write inside startTransition — only state updates that render.
  2. 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.
  3. 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.