Topic 16.7
Concurrent Rendering: useTransition and useDeferredValue
In one line
Concurrent rendering lets React prepare a slow update in the background and interrupt it for urgent ones; useTransition marks a state update as non-urgent and gives you isPending, and useDeferredValue lets a slow part of the screen lag behind a fast-changing value so typing stays responsive.
Think of it like this
A restaurant kitchen. A big catering order (200 sandwiches) arrives, and so does one customer who wants a glass of water. A good kitchen doesn't make the customer wait for 200 sandwiches: it hands over the water at once and keeps working on the big order in between. If the catering order is changed halfway, it starts the new version and throws away the half-made old one. Urgent work first, non-urgent work in the gaps.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Concurrent rendering
- React's ability to prepare a render in the background, pause it, and resume or restart it, without blocking the page.
- Urgent update
- An update that must show at once, like the letter you just typed.
- Transition
- A non-urgent update React may render in the background and interrupt, like showing a new tab's content.
- isPending
- A boolean from useTransition that is true while a transition is still being prepared.
- startTransition
- The function that marks the state updates inside it as a transition.
- Deferred value
- A copy of a value, from useDeferredValue, that may lag behind the latest one while React catches up.
- Interruptible
- Work React can stop halfway when something more urgent arrives, and restart later.
Step by step
01Find the slow update
Clicking a tab renders 200 slow posts. Without a transition, the click handler's update is urgent: React renders all 200 before the browser can paint anything, so the page freezes and clicks on other tabs feel ignored.
02Wrap it in startTransition
Mark the tab change as a transition. React shows the click immediately (with isPending) and renders the posts in the background.
const [isPending, startTransition] = useTransition();
function choose(next) {
startTransition(() => {
setTab(next); // non-urgent: may be interrupted
});
}03Two renders, two priorities
The urgent render shows the old tab with isPending = true. The transition render shows the new tab with isPending = false. If the user clicks another tab meanwhile, React drops the half-done render and starts the newest one.
04Defer a value you don't control
For search, keep the input urgent and give the results a deferred copy of the query. Wrap the results in memo so the urgent render can skip them.
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowResults query={deferredQuery} /> {/* memo */}
</div>
</>
);05Choose between them
Own the state update? useTransition (and you get isPending). Only receive the value, or need the input itself to stay urgent? useDeferredValue. Both use the same machinery underneath.
Try it yourself
- 1
Remove the transition
In the tabs example, change
chooseto callsetTab(next)directly. Predict the log: noisPending = trueline, one render straight to Posts. Click Posts and then Home quickly by hand: the page freezes until Posts finishes. - 2
Remove the memo
In the deferred example, replace
memo(function SlowList ...)with a plain function. Predict the log after typing "a": SlowList now renders in the urgent pass too (with the old query), so the deferral gains nothing. - 3
Show staleness
Add
{stale && <p>Updating...</p>}under the input. Type quickly and watch it appear while the list catches up.
Code & diagrams
One click, two renders: first urgent (old tab, isPending true), then the transition (new tab). Click Posts then Home quickly yourself: Home wins at once, because React drops the unfinished Posts render. Each post takes 1 ms in this development build; production is faster, but the pattern is the same.
Preview
Console
Nothing logged yet.Expected console output
App rendered: tab = home, isPending = false
--- click Posts
App rendered: tab = home, isPending = true
App rendered: tab = posts, isPending = falseTyping "a" renders App twice: first urgently with the old deferred value (SlowList is skipped, so the letter appears at once), then in the background with the new one. Type fast yourself: the list skips intermediate values and only renders for what you stopped at.
Preview
Console
Nothing logged yet.Expected console output
App rendered: query = "", deferred = ""
SlowList rendered for ""
--- type a
App rendered: query = "a", deferred = ""
App rendered: query = "a", deferred = "a"
SlowList rendered for "a"// Wrong: the input's value is now a non-urgent update.
// Typing can lag or drop characters while a background render runs.
<input value={query} onChange={(e) => startTransition(() => setQuery(e.target.value))} />
// Right: urgent input, non-urgent results.
<input
value={query}
onChange={(e) => {
setQuery(e.target.value); // urgent
startTransition(() => setResultsQuery(e.target.value)); // can wait
}}
/>Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Transition-wrapped controlled input
Write onChange={(e) => startTransition(() => setQuery(e.target.value))} in the deferred example and type quickly.
Myth vs fact
Myth
useTransition makes rendering faster.
Fact
It doesn't reduce the work. It keeps the page responsive while the work happens and lets React throw away outdated work.
Myth
useDeferredValue is a debounce.
Fact
There's no fixed delay. React renders the deferred value as soon as it can, and interrupts it if a newer value arrives. Fast devices see almost no lag.
Myth
Concurrent rendering means React uses multiple threads.
Fact
Everything still runs on the main thread. "Concurrent" means React can switch between several pending renders by pausing and resuming.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Transitions use the TransitionLanes in React's lane model; updates inside them are batched together, and a newer transition supersedes an older unfinished one. React yields to the browser roughly every 5 ms during concurrent rendering (the scheduler's frame interval).
- ▸
The background render of
useDeferredValueis itself a transition: if it suspends, the deferred UI keeps showing the old content instead of a fallback. Combined with Suspense this is the standard pattern for search-as-you-type without flashing spinners. - ▸
Since React 19,
startTransitionaccepts async functions (Actions). State updates after anawaitinside it are not automatically marked as transitions in React 19; wrap them in anotherstartTransition(the docs call this out as a current limitation). - ▸
Transitions don't help code that isn't React rendering: a single 200 ms synchronous function in an event handler still blocks. Split such work, move it to a Web Worker, or yield with
scheduler.yield()where supported.
Remember this
- 1
Urgent vs non-urgent updates. Some updates must show immediately because they reflect what the user just did with their hands: typing a character, clicking a button, pressing a key. These are urgent. Others can wait a moment: the filtered results below a search box, the content of a newly chosen tab, a chart redrawn for a new date range. Users expect those to take a little time. React calls such updates transitions: moving the UI from one view to another.
- 2
Concurrent rendering (React 18+) means React can work on a render without blocking the browser. For a transition, React renders in small slices (one fiber at a time, Topic 16.2), checking about every 5 ms whether something more urgent arrived. If you type, React pauses the transition, handles the keystroke, and then restarts the transition with the newest state. The old, half-finished render is thrown away and never reaches the screen. A normal (urgent) render, by contrast, runs to the end once started.
- 3
**
useTransition**:const [isPending, startTransition] = useTransition(). Wrap the state update that causes slow rendering:startTransition(() => setTab("posts")). React first renders urgently with **isPending = true** (and the old tab), so you can dim the old content or show a spinner, then renders the new tab in the background and commits it withisPending = false. The function you pass runs immediately; only the state updates inside it are marked as transitions. Since React 19 the function may be async (an Action, Topic 19.2), andisPendingstays true until it finishes. There's also a standalonestartTransitionimport for code outside components, withoutisPending. - 4
**
useDeferredValue**:const deferredQuery = useDeferredValue(query). You get a copy of a value that may lag behind. Whenquerychanges, React first renders with the old deferred value (fast: the slow part can be skipped), then tries a background render with the new value, which is interruptible like a transition. Use it when you don't control the state update (the value comes in as a prop) or when the input itself must stay urgent but the results can lag. The slow child must be wrapped inmemo(or compiled), otherwise it re-renders in the urgent pass anyway and nothing is gained. Comparequery !== deferredQueryto show that results are stale. React 19 added aninitialValueargument:useDeferredValue(value, initialValue)for the first render. - 5
What they are not. Transitions don't make slow code faster: the total work is the same or slightly more. They make the app stay responsive while the work happens. They also aren't debouncing: there's no fixed delay. On a fast device the deferred render happens almost at once; on a slow one it lags more. And controlled text inputs must never be updated in a transition: the input's own value must update urgently or typing breaks, so keep
setQueryurgent and defer the results. - 6
Transitions and Suspense. If a transition causes a component to suspend (Topic 14.3), React keeps the old UI on screen instead of showing the nearest fallback again, and
isPendingstays true until the new content is ready. That's why routers wrap navigations in transitions: the old page stays visible while the next one loads (Topic 16.8 uses this for lazy components).
Explain it without notes
What is the difference between an urgent update and a transition?
What happens, step by step, when you call startTransition(() => setTab("posts"))?
When would you choose useDeferredValue over useTransition?
Why must the slow component be memoised for useDeferredValue to help?
Practice
A FilterPanel changes category state and a big ProductGrid re-renders slowly. Make category changes non-blocking and dim the grid while pending.
A Results component receives query as a prop from a parent you can't change. Make the input above it stay responsive.
Trade-offs
- ↔
Transitions keep input responsive but may render more in total (interrupted work is thrown away) and briefly show stale content, which you must make visible with
isPendingor a stale check. - ↔
Concurrent features need components that are pure and safe to render more than once; code with side effects in render can misbehave.
Done when you can
I can tell urgent updates from transitions.
I can use useTransition and show a pending state with isPending.
I can use useDeferredValue with a memoised slow component.
I know never to put a controlled input's own update in a transition.
I can explain how transitions interact with Suspense.