Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A2
35 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛A2 · Part 3 · Advanced

Concurrent features

Telling React which updates can wait — so the urgent one never queues behind them.

6 min read12 sections2 exercises
35 of 57

In this chapter

  • The problem
  • startTransition
  • useTransition versus startTransition
  • useDeferredValue
  • Activity : keeping a subtree alive but hidden

and 7 more

about 6 min
12On this page
  1. The problem
  2. startTransition
  3. useTransition versus startTransition
  4. useDeferredValue
  5. Activity : keeping a subtree alive but hidden
  6. Automatic batching
  7. Transitions in routing
  8. What "interruptible" means in milliseconds
  9. Reading it in DevTools
  10. Separate transitions stay separate, since React 19.3
  11. The catch worth knowing
  12. Where this shows up without you asking

The problem

Type into a search box that filters ten thousand rows. Every keystroke sets two pieces of state: the input value, and the filtered list. React renders both together, the filtering takes 200ms, and the character you typed appears a fifth of a second later. The input feels broken.

Nothing here is fixable by making the filter faster — the problem is that both updates were treated as equally urgent. Concurrent features let you say they are not.

startTransition

const [isPending, startTransition] = useTransition();

function handleChange(e) {
  setQuery(e.target.value);                    // urgent: show the character
  startTransition(() => {
    setResults(filter(e.target.value));        // can wait, can be abandoned
  });
}

The input updates immediately. The list update runs at lower priority, and if another keystroke arrives mid-render React throws that work away and starts again with the newer value. You never render a list for a query the user has already moved past.

isPending is true while the transition is in flight — use it to dim the stale results rather than to show a spinner. The old content staying visible is the point.

useTransition versus startTransition

const [isPending, startTransition] = useTransition();   // the hook
import { startTransition } from "react";                 // the standalone
useTransitionstartTransition
Gives you isPendingyesno
Callable outside a componentno — it is a hookyes
Use itwhen the UI shows pending statein a store, a router, an event outside React
<div style={{ opacity: isPending ? 0.6 : 1 }}>
  <Results items={results} />      // dim the stale list, do not blank it
</div>

isPending is the reason to prefer the hook. Replacing the results with a spinner throws away content the user was reading; dimming it says "working" without taking anything away. That distinction is most of what makes a transition feel different from a loading state.

Async work inside a transition keeps isPending true until the awaited work settles, so an action that saves and then updates state stays pending across the whole operation rather than flickering between the two.

useDeferredValue

const deferred = useDeferredValue(query);
const results = useMemo(() => filter(deferred), [deferred]);

<input value={query} onChange={...} />      // always current
<Results items={results} />                 // lags behind, deliberately

Same outcome from the other end. Instead of marking the update, you mark the value: React renders once with the old value at high priority, then again with the new one at low priority.

UseWhen
startTransitionYou control the state update — you can wrap the setter
useDeferredValueThe value arrives as a prop, or the update is not yours to wrap
Not a debounce

A debounce waits a fixed time and hopes it is enough. A transition starts immediately and yields only when something more urgent appears — so on a fast machine the results are instant, and on a slow one the input still never stutters. It adapts; a 300ms timeout does not.

<Activity>: keeping a subtree alive but hidden

<Activity mode={tab === "chat" ? "visible" : "hidden"}>
  <ChatPanel />
</Activity>

Stable since React 19.2. A hidden Activity is not unmounted — its state, scroll position and DOM stay alive, its effects are torn down, and React renders it at the lowest priority in the background. Switching back is instant and nothing was lost.

ApproachState keptEffects runCost
Unmount itlostnorebuild on return
display: nonekeptyes — still runningtimers and subscriptions stay live
<Activity mode="hidden">kepttorn downmemory only

The middle row is what most tab implementations do today, and it is why a hidden panel keeps polling. Activity is the first API that gives you "keep the state, stop the work" without writing it yourself — and it can pre-render a hidden route so the next navigation has nothing left to do.

Automatic batching

setA(1); setB(2);                            // one render, always
setTimeout(() => { setA(1); setB(2); });     // one render since React 18
fetch(url).then(() => { setA(1); setB(2); }); // one render since React 18

Before 18, batching only happened inside React event handlers; updates in timeouts, promises and native handlers each rendered separately. Now everything batches. flushSync opts out for the rare case where you must read the DOM between two updates.

Transitions in routing

startTransition(() => navigate("/dashboard"));

Wrapping a navigation keeps the current page interactive while the next one renders, rather than blanking to a spinner the moment the link is clicked. Modern routers do this for you; knowing why matters when you have to debug it.

What "interruptible" means in milliseconds

React works in slices of roughly five milliseconds, then checks whether it should yield. If the browser has input waiting, React stops, lets the browser handle it, and resumes afterwards — possibly discarding what it had if that input made the work irrelevant.

Which sets the limit. React can only yield between units of work. One component that spends 300ms in a single synchronous loop offers no yield point, so a transition cannot help it. The fix there is to make the unit smaller: memoise the calculation, virtualise the list, or move the work to a Web Worker.

Reading it in DevTools

React 19.2 adds custom tracks to the Chrome performance panel: a Scheduler track showing which priority React was working at, and a Components track showing render and effect phases per component.

What to look for: a long unbroken block at high priority is work that should have been a transition; repeated identical renders in the Components track is something re-rendering that did not need to. Both are visible there and nowhere else.

Separate transitions stay separate, since React 19.3

Until 19.3, React entangled concurrent transitions into a single render. Start a slow one — a filter over a large list — and then a quick one — switching a tab — and the tab switch could not commit until the filter was finished too.

startTransition(() => setFilter(q));        // slow: re-renders 10,000 rows
startTransition(() => setTab("settings"));  // fast: no longer waits for the filter

React 19.3 renders transitions independently, so a slow transition no longer holds up unrelated ones. Nothing in your code changes. What changes is the debugging: if you profiled an app on 19.2 and saw a quick update stuck behind a slow one, that was the entanglement, and upgrading is the fix.

The catch worth knowing

A transition can be interrupted and restarted, so anything inside it may run more than once. That is fine for pure state updates and wrong for anything with a side effect — do not put a fetch or an analytics call inside startTransition.

It also cannot help with a single synchronous block that is slow. Transitions let React yield between units of work; one component that spends 300ms in a loop still blocks, because there is no yield point inside it. Fix that with memoisation, virtualisation, or a Web Worker.

Where this shows up without you asking

  • Suspense uses transitions to avoid replacing visible content with a fallback.
  • Server Components stream in at transition priority.
  • Framework routers wrap navigation in one already.
Interview answer, one sentence

"Concurrent features are a priority system: startTransition marks an update as interruptible so an urgent one — a keystroke — is never queued behind it, and React abandons in-progress low-priority work when newer input arrives. It is not a debounce, because it yields on demand rather than waiting a fixed time."

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 5 testsKeep typing responsive while the list catches upadvanced
  • 5 testsLet the list lag one step behind the inputadvanced
All 54 for this level →
A2

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A1Previous · A1Reconciliation & Fiber
  2. A2you are here
  3. Next · A3Suspense & code splittingDeclaring where a loading state lives, instead of threading one through every component.6 min read

01On this page

12 sections · 0%

  1. 01The problem
  2. 02startTransition
  3. 03useTransition versus startTransition
  4. 04useDeferredValue
  5. 05Activity : keeping a subtree alive but hidden
  6. 06Automatic batching
  7. 07Transitions in routing
  8. 08What "interruptible" means in milliseconds
  9. 09Reading it in DevTools
  10. 10Separate transitions stay separate, since React 19.3
  11. 11The catch worth knowing
  12. 12Where this shows up without you asking

[] chapters · t top