Command Palette

Search for a command to run...

Hectal
PHASE 16Advanced React 18+ ~22 min· topic 7 of 10

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.

Tabs.jsxwhole filejsx
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.

Two renders, two prioritiesdiagram
Rendering diagram…

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.

Search.jsxwhole filejsx
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. 1

    Remove the transition

    In the tabs example, change choose to call setTab(next) directly. Predict the log: no isPending = true line, one render straight to Posts. Click Posts and then Home quickly by hand: the page freezes until Posts finishes.

  2. 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. 3

    Show staleness

    Add {stale && <p>Updating...</p>} under the input. Type quickly and watch it appear while the list catches up.

Code & diagrams

useTransition: the old tab stays while the new one renders New tab

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

Press Run to see the page.

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 = false
useDeferredValue: the input stays fast, the list catches up New tab

Typing "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

Press Run to see the page.

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"
Never put the input's own update in a transitionjsx
// 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.

terminal
$ Preview
── what you'll see ──
The text box lags behind your typing; while the list renders, letters appear late.

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 useDeferredValue is 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, startTransition accepts async functions (Actions). State updates after an await inside it are not automatically marked as transitions in React 19; wrap them in another startTransition (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. 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. 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. 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 with isPending = 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), and isPending stays true until it finishes. There's also a standalone startTransition import for code outside components, without isPending.

  4. 4

    **useDeferredValue**: const deferredQuery = useDeferredValue(query). You get a copy of a value that may lag behind. When query changes, 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 in memo (or compiled), otherwise it re-renders in the urgent pass anyway and nothing is gained. Compare query !== deferredQuery to show that results are stale. React 19 added an initialValue argument: useDeferredValue(value, initialValue) for the first render.

  5. 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 setQuery urgent and defer the results.

  6. 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 isPending stays 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

01

What is the difference between an urgent update and a transition?

02

What happens, step by step, when you call startTransition(() => setTab("posts"))?

03

When would you choose useDeferredValue over useTransition?

04

Why must the slow component be memoised for useDeferredValue to help?

Practice

01

A FilterPanel changes category state and a big ProductGrid re-renders slowly. Make category changes non-blocking and dim the grid while pending.

02

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 isPending or 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.