How it works
The explanation, with examples you can run.
Transitions
import { useState, useTransition } from "react" declare function SlowList(props: { query: string }): React.ReactNode export function Search() { const [text, setText] = useState("") const [query, setQuery] = useState("") const [isPending, startTransition] = useTransition() return ( <> <input value={text} onChange={(e) => { setText(e.target.value) // urgent: the input must update now startTransition(() => setQuery(e.target.value)) // can wait }} /> <div style={{ opacity: isPending ? 0.6 : 1 }}> <SlowList query={query} /> </div> </> )}Without the transition, each keystroke renders SlowList synchronously before the browser can show the character, so typing stutters. With it, React commits the urgent setText first. It then renders the list as a transition, regularly yielding to the browser so that input can be handled. If another keystroke arrives, React drops the half-finished render and starts again with the newer query. Rendering still happens on the main thread; a transition makes the work interruptible, not faster. A controlled input's own state must not be set in a transition, or typing would lag.
useDeferredValue(query) does the same from the reading side: it gives you a copy of a value that lags behind during urgent updates, which suits values you receive as props.
In React 19, the function passed to startTransition may be async. React calls the function passed to startTransition an action, and isPending stays true until it finishes, which makes it a simple way to show progress for a form submission or a save. State updates after an await currently need their own startTransition wrapper to count as part of the transition.
Suspense
import { lazy, Suspense } from "react" const Chart = lazy(() => import("./Chart")) export function Dashboard() { return ( <Suspense fallback={<p>Loading chart...</p>}> <Chart /> </Suspense> )}While Chart's code is loading, Chart suspends: React stops rendering that part of the tree and shows the nearest boundary's fallback. When the code arrives, it renders the real content in its place. What can suspend: components from lazy, promises read with use(promise) (the promise must be cached, not created during render), and data loading in Suspense-enabled frameworks. Fetching in an effect or an event handler does not activate Suspense.
Boundaries are a design decision. One boundary around a page gives one loading state; several give independent ones that appear as each is ready. During streaming server rendering, each boundary is also a point where the server can send the shell first and the rest later.
Transitions and Suspense together
When an update that is not a transition causes already-visible content to suspend, React has to hide it behind the fallback: the page you were reading turns into a spinner. When the update is a transition, React keeps the old content on screen, with isPending true, until the new content is ready. Routers built for React use transitions for navigation for this reason. New boundaries that appear in the update still show their fallbacks, so the next screen can show its own skeletons without hiding the current one.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.