Suggest an editImprove this articleRefine the answer for “Concurrent rendering in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Concurrent rendering in React** lets React 18 pause a render in progress to handle urgent updates first, like typing, then resume or discard the paused work. Enable it with `createRoot` and mark slow updates with `startTransition`: ```tsx const [isPending, startTransition] = useTransition(); startTransition(() => setResults(filter(items, query))); // runs as low-priority ``` **Key:** user input and clicks always interrupt transitions. The list catches up when the browser has spare time.Shown above the full answer for quick recall.Answer (EN)Image**Concurrent rendering in React** is React 18's ability to pause a render mid-work, handle a more urgent update first, then resume or discard the paused work entirely. ## Theory ### TL;DR - Analogy: a chef who stops slow-roasting a dish to plate a walk-in order, then returns to the roast. React pauses low-priority renders when you type or click. - Before React 18, every render blocked the browser until it finished. Concurrent mode breaks work into ~5ms slices and yields to the browser between each one. - Main API: wrap slow state updates in `startTransition`. That covers 80% of real-world concurrent usage. - Decision rule: use it for heavy lists, search filters, or tab switching with complex views. Static pages don't need it. ### Quick example ```jsx import { useState, useTransition } from 'react'; import { createRoot } from 'react-dom/client'; function Search({ items }) { const [query, setQuery] = useState(''); const [filtered, setFiltered] = useState(items); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { setQuery(e.target.value); // urgent: updates the input right away startTransition(() => { // non-urgent: filtering 10k items won't block typing setFiltered(items.filter(i => i.includes(e.target.value))); }); }; return ( <div> <input value={query} onChange={handleChange} /> {isPending && <span>Updating list...</span>} <ul>{filtered.map((item, i) => <li key={i}>{item}</li>)}</ul> </div> ); } const root = createRoot(document.getElementById('root')); root.render(<Search items={Array(10000).fill('item')} />); ``` Without `startTransition`, typing in that input lags 100-500ms on a mid-range device. With it, the input responds instantly and the list catches up. ### Synchronous vs concurrent rendering Pre-18 React commits the entire component tree before returning control to the browser. Type a character while 10,000 items are rendering and the keystroke sits in a queue. React 18's scheduler breaks rendering into ~5ms chunks and checks between each chunk whether a higher-priority update arrived. If you typed again, the old render is discarded. React starts fresh with the latest value and commits only that result. ### When to use - Search or filter over large datasets: `startTransition` keeps input responsive while the list updates in the background. - Tab switching with heavy views: mark the new tab's render as a transition so the current view doesn't freeze. - Rapid dropdowns or user profile switching: concurrent mode discards stale renders automatically, removing the need for manual request ID tracking. - Static pages or simple dashboards: synchronous rendering is fine. No overhead needed. - Legacy codebases: only add concurrent features where profiling shows actual jank. Check render durations in React DevTools Profiler first. ### How it works internally React 18 assigns every update a "lane," a 32-bit bitmask where lower bit values mean higher priority. A keystroke goes into `InputContinuousLane`. A `startTransition` update goes into `TransitionLane`. The scheduler runs a `shouldYield()` check roughly every 5ms, aligned to a 60fps frame budget. If a higher-priority update arrives mid-render, React stops building the current fiber tree, stores the half-built alternate tree in memory, and starts fresh. When the urgent work commits, React either resumes the paused work or garbage-collects it if state has changed since. Nothing commits partially. The DOM always reflects the last completed render. This runs in the browser's JS engine using a polyfill of `requestIdleCallback`. On the server, Node.js has no main thread that yields to input events. Use `renderToPipeableStream` with Suspense instead, which streams HTML chunks as data resolves. ### Concurrent features at a glance | Feature | What it does | API | |---|---|---| | Transitions | Marks updates as interruptible and low-priority | `useTransition`, `startTransition` | | Deferred values | Delays re-render of a derived value | `useDeferredValue` | | Automatic batching | Groups all state updates into one render, even in async code | Built into React 18 | | Streaming SSR | Sends HTML in chunks as Suspense boundaries resolve | `Suspense` + `renderToPipeableStream` | | Selective hydration | Hydrates parts of the page independently | `Suspense` on client | ### Enabling concurrent rendering ```jsx // React 18: one line unlocks all concurrent features import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(<App />); // Old API: stays fully synchronous // ReactDOM.render(<App />, document.getElementById('root')); ``` That one change is the gate. Automatic batching kicks in immediately. Each other feature (`useTransition`, `useDeferredValue`, `Suspense`) is opt-in per usage and won't affect code that doesn't use it. ### Common mistakes **1. Wrapping trivial state in `startTransition`.** ```jsx // no benefit: a counter update is already fast startTransition(() => setCount(count + 1)); ``` Transitions add overhead: pending state tracking, interrupt handling, alternate tree work. On simple state, this can add 10-20% extra cost for zero visible gain. Profile before wrapping anything. **2. Ignoring `isPending`.** ```jsx // wrong: user stares at a stale list with no feedback <input onChange={e => startTransition(() => setFilter(e.target.value))} /> ``` During a transition, the old content stays visible. Without a spinner or loading indicator, users think the app is frozen. Always check `isPending` and show something. **3. Calling `flushSync` inside a loop.** ```jsx // reverts to pre-18 blocking behavior items.forEach(item => flushSync(() => updateItem(item))); ``` `flushSync` forces a synchronous commit and bypasses the scheduler. In a loop, each iteration blocks the browser. Use it once after a loop if you genuinely need an immediate commit. **4. Assuming transitions run in arrival order.** Concurrent mode prioritizes by lane, not by the order `startTransition` was called. Two rapid transitions may result in only the second one committing. Design state updates to be idempotent and test with `act` in Jest to catch ordering issues. The most common miss is number 2. Adding `isPending` after the fact is the fastest fix I've seen resolve a "why does my UI feel broken" bug in code that was otherwise correct. ### Real-world usage - Next.js 13+ App Router enables concurrent rendering by default for React Server Components and streaming - TanStack Query's `useSuspenseQuery` integrates with Suspense boundaries and transitions for data fetch UX - React DevTools Profiler shows lane assignments and time slices so you can see exactly which renders get interrupted - Meta's production feed handles infinite scroll and live text input simultaneously using concurrent scheduling ### Follow-up questions **Q:** What is a "lane" in React's scheduler? **A:** A 32-bit bitmask that encodes update priority. Lower bit values mean higher priority. `InputContinuousLane` beats `TransitionLane`. React groups updates with the same lane and flushes the highest-priority group first. **Q:** How does `startTransition` differ from `flushSync`? **A:** They are opposites. `startTransition` marks work as low-priority and interruptible. `flushSync` forces a synchronous commit before returning. Using both on the same update cancels out the transition. **Q:** How long is a time slice in concurrent mode? **A:** About 5ms, measured by `Scheduler.unstable_shouldYield()`. That fits within the 16ms frame budget for 60fps, leaving time for browser paint. **Q:** What happens to a render that gets discarded? **A:** The alternate fiber tree gets garbage collected. React never commits partial work, so the DOM always reflects a complete, consistent render. **Q:** Why doesn't concurrent rendering apply to SSR the same way? **A:** Node.js has no main thread that yields to input events. The server-side equivalent is `renderToPipeableStream`, which streams HTML chunks to the client as Suspense boundaries resolve. **Q:** In a custom renderer without `requestIdleCallback`, how would you implement yielding? **A:** Use `setTimeout(0)` with a priority queue and track frame budget via `performance.now()`. Hook into the Reconciler's `scheduleCallback` so React's own scheduler drives when work runs. Mentioning `scheduleCallback` from the Scheduler package specifically is what interviewers at this level want to hear. ## Examples ### Basic: createRoot setup ```jsx import { createRoot } from 'react-dom/client'; import App from './App'; const root = createRoot(document.getElementById('root')); root.render(<App />); ``` One line replaces `ReactDOM.render`. After this, React 18 automatic batching is active and `useTransition` becomes available anywhere in the tree. ### Intermediate: todo search with loading feedback ```jsx import { useState, useTransition } from 'react'; function TodoSearch({ todos }) { const [query, setQuery] = useState(''); const [results, setResults] = useState(todos); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; setQuery(value); // always immediate startTransition(() => { setResults(todos.filter(todo => todo.text.includes(value))); }); }; return ( <div> <input value={query} onChange={handleChange} placeholder="Search todos..." /> {isPending ? <p>Filtering...</p> : null} <ul> {results.map(todo => <li key={todo.id}>{todo.text}</li>)} </ul> </div> ); } ``` `isPending` is `true` between the moment `startTransition` fires and when the filtered list commits to the DOM. That gap is your window to show a spinner, skeleton, or a text label. ### Senior: rapid profile switching without stale data ```jsx import { useState, useTransition, Suspense } from 'react'; function UserProfile({ userId }) { const [profile, setProfile] = useState(null); const [isPending, startTransition] = useTransition(); const loadProfile = (id) => { startTransition(async () => { const data = await fetchProfile(id); setProfile(data); // if userId changed mid-fetch, React discards this render }); }; return ( <div style={{ opacity: isPending ? 0.6 : 1 }}> <Suspense fallback={<div>Loading profile...</div>}> {profile ? ( <ProfileCard data={profile} /> ) : ( <button onClick={() => loadProfile(userId)}>Load profile</button> )} </Suspense> </div> ); } ``` If a user clicks five different profiles rapidly, React discards the four intermediate renders and commits only the latest. Pre-18, you needed a manual "stale request" check: track the current request ID and ignore responses from outdated fetches. Concurrent mode removes that pattern for most cases.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.