Concurrent Features: Transitions & Suspense
โฑ๏ธ ~4-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐๏ธ Concurrent React = a DJ with two decks. Can mix the next track (new render) without interrupting the current one. startTransition = "play this next, but not urgently."
Synchronous React (before Concurrent)
One render at a time. React starts rendering โ must finish before any other work. Like a single-deck DJ: must finish the current track before starting the next. User input waits in line.
Concurrent React (React 18+)
Multiple renders can be in-flight at different priorities. High-priority (user input) interrupts low-priority (list update). React can pause, resume, or abandon in-progress renders.
The priority system
Interactive Sandbox
โ Move something, see it react instantly.Concurrent Feature
Scenario
Filtering 10,000 items while the user types in a search box
Every keystroke triggers a synchronous re-render of 10,000 items. The browser can't process new keystrokes until the render completes โ input lags visibly at 200ms+ per render.
| 1 | function Search() { |
| 2 | const [query, setQuery] = useState(''); |
| 3 | const [results, setResults] = useState(allItems); |
| 4 | const [isPending, startTransition] = useTransition(); |
| 5 | ย |
| 6 | function handleChange(e: React.ChangeEvent<HTMLInputElement>) { |
| 7 | // Input update: synchronous (high priority) |
| 8 | setQuery(e.target.value); |
| 9 | ย |
| 10 | // List update: transition (can be interrupted) |
| 11 | startTransition(() => { |
| 12 | setResults(allItems.filter((item) => |
| 13 | item.name.includes(e.target.value) |
| 14 | )); |
| 15 | }); |
| 16 | } |
| 17 | ย |
| 18 | return ( |
| 19 | <> |
| 20 | <input value={query} onChange={handleChange} /> |
| 21 | {isPending && <Spinner />} |
| 22 | <ItemList items={results} /> |
| 23 | </> |
| 24 | ); |
| 25 | } |
Challenge
Compare the 'without' and 'with concurrent' descriptions for all 5 patterns.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: What is the difference between startTransition and setTimeout for deferring work?
setTimeout delays work by a fixed duration regardless of what the browser is doing. startTransition defers work to when the React scheduler has capacity โ it's priority-based, not time-based. React can interrupt a transition render if higher-priority work (user input) arrives. setTimeout cannot be interrupted once the timeout fires.
Q: How does Suspense know when to show the fallback?
Suspense uses a protocol where components "throw" a Promise during render. When React encounters a thrown Promise, it shows the nearest Suspense fallback. When the Promise resolves, React retries the render. Libraries like TanStack Query, React.lazy, and React 19's use() all implement this protocol.
Q: What is "tearing" and why does Concurrent Mode need useSyncExternalStore?
| 1 | // Tearing: different parts of the UI show different |
| 2 | // values from the same external store during one render |
| 3 | ย |
| 4 | // Concurrent React can pause mid-render. |
| 5 | // If an external store updates during the pause: |
| 6 | // โ some components render with old value |
| 7 | // โ some render with new value |
| 8 | // โ UI is torn (inconsistent state visible to user) |
| 9 | ย |
| 10 | // Solution: useSyncExternalStore ensures atomic snapshots |
| 11 | const count = useSyncExternalStore( |
| 12 | store.subscribe, // subscribe to changes |
| 13 | store.getSnapshot, // get consistent snapshot |
| 14 | ); |
| 15 | // Used internally by Redux, Zustand, Jotai |
The Deep Dive
โ Spec refs, engine internals, the minutiae.React scheduler internals
React's scheduler uses MessageChannel to yield control back to the browser between render chunks. MessageChannel callbacks run between paint frames โ higher priority than setTimeout (which has a minimum 4ms delay) but lower priority than microtasks. This allows the browser to process input events between chunks of React rendering work.
Priority lanes
React uses a bitmask "lanes" system to track priority. Each update is assigned one or more lanes (SyncLane, TransitionLane, IdleLane, etc.). The scheduler picks the highest-priority non-empty lane and works on it. When a high-priority lane arrives, the current low-priority render is interrupted and the high-priority update is processed first. The interrupted work is resumed when no higher-priority lanes are pending.
Suspense protocol โ promise throwing
| 1 | // Simplified Suspense protocol (how libraries implement it) |
| 2 | function useSuspenseData(key) { |
| 3 | const cache = getCache(); |
| 4 | const entry = cache.get(key); |
| 5 | ย |
| 6 | if (!entry) { |
| 7 | // Throw a Promise โ React catches it, shows fallback |
| 8 | const promise = fetchData(key).then((data) => { |
| 9 | cache.set(key, { status: 'resolved', data }); |
| 10 | }); |
| 11 | cache.set(key, { status: 'pending', promise }); |
| 12 | throw promise; // โ Suspense protocol |
| 13 | } |
| 14 | ย |
| 15 | if (entry.status === 'pending') throw entry.promise; |
| 16 | if (entry.status === 'error') throw entry.error; |
| 17 | return entry.data; // resolved โ return the data |
| 18 | } |
Interview Questions
โ Real questions from real interviews โ with answers.startTransition is priority-based and interruptible; setTimeout delays by a fixed time and is not interruptible.
A component 'throws' a Promise during render; React catches it and shows the nearest Suspense fallback until the Promise resolves.
Tearing: different components read different snapshots of an external store in one render, causing UI inconsistency.
useTransition when you own the setState call; useDeferredValue when you only receive a prop/value.
Text input value must update synchronously or the input becomes unresponsive and shows the wrong character.
React uses a bitmask 'lanes' system; high-priority lanes (SyncLane) interrupt and preempt low-priority ones (TransitionLane).
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is React's scheduler responsible for?