Course outline

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.

About 4 min readContent reviewed
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.

jsx Component module — requires the course React host
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>; }
Expected output
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.

jsx Component module — requires the course React host
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>; }
Expected output
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.

Official references