Back to React
React
medium
senior

What are the concurrent features introduced in React 18 and why do they matter?

React 18+ can render in the background, interrupt itself, and prioritize urgent updates. The primitives: `useTransition` / `startTransition` (mark non-urgent updates), `useDeferredValue` (lag a value to keep input responsive), Suspense for data + code streaming, and automatic batching across async boundaries. They don't make React faster — they let you schedule work so that user input always wins.

9 min read·~15 min to think through

Pre-18 React was synchronous. setState started a render that ran to completion before the browser could repaint. Slow renders → janky inputs. React 18 introduces a concurrent renderer: render work can be paused, resumed, and discarded. Combined with new primitives, you can mark which updates are urgent vs. interruptible.

The four big primitives

1. useTransition / startTransition.

Mark a state update as non-urgent. The transition can be interrupted by urgent updates (typing, clicks).

tsx
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);

function handleChange(e) {
  const next = e.target.value;
  setQuery(next);                          // urgent — input updates immediately
  startTransition(() => {
    setResults(expensiveSearch(next));     // non-urgent — can be interrupted
  });
}

If the user keeps typing, React abandons the in-progress search render and starts a new one. The input stays responsive. isPending lets you show a subtle "filtering…" indicator without blocking.

Use for: filtering large lists, switching tabs that render expensive panels, navigation in client-side routing, form submission that triggers data refetch.

2. useDeferredValue.

A "consumed" version of a value that lags behind the source. Use when you can't wrap the setter in a transition (because someone else owns the state).

tsx
function SearchResults({ query }) {
  const deferred = useDeferredValue(query);
  // `deferred` updates after urgent renders settle
  return <ResultList query={deferred} />;
}

The parent updates query urgently; the child re-renders with a deferred copy. The child's render is interruptible.

Like a built-in debouncer, but timing-free — it adapts to render cost. Pair with React.memo so the child can bail out when the deferred value equals the previous.

3. Suspense for code and data.

Suspense boundaries let a component "suspend" (throw a promise). React shows the fallback while the promise settles, then renders the component. Combined with streaming SSR, content can appear progressively:

tsx
<Suspense fallback={<Skeleton />}>
  <SlowChart />
</Suspense>

Works with: React.lazy (code splitting), data fetching frameworks (Next.js, Relay, TanStack Query's useSuspenseQuery), and the new use() hook in 19+.

4. Automatic batching.

In React 17, multiple setStates inside an event handler batched, but inside async (setTimeout, fetch) they didn't — each triggered a separate render. React 18 batches across all contexts.

tsx
async function onSubmit() {
  setIsSubmitting(true);
  const data = await api.post();
  // In 17: two re-renders here. In 18: batched into one.
  setIsSubmitting(false);
  setData(data);
}

flushSync is the escape hatch when you need to opt out (rare — e.g., DOM measurement between updates).

The mental shift

  • Urgent updates: input typing, hover, click feedback. Must complete before the next paint.
  • Transitions: results of those updates that take time. Can be interrupted, deferred, or dropped if superseded.

Saying "mark this update as a transition" is saying "the user doesn't need this to be immediate; what they need immediate is the cause (their input), not the effect (the search results)."

What it doesn't do

  • Doesn't make code faster. A 100ms render is still 100ms. It just doesn't block input handling for 100ms.
  • Doesn't replace memoization. Transitions + slow components without memo still re-render a lot.
  • Doesn't help if your bottleneck is the main thread doing non-React work. Heavy synchronous JS during render still blocks.

Strict Mode interaction

Strict Mode in development renders effects and components twice (with state preserved). It catches:

  • Side effects in render (which break under interruption + retry).
  • Effects that don't clean up properly.

Concurrent rendering can re-run renders; impure components will misbehave. Strict Mode flushes these out before production.

<Activity> (formerly <Offscreen>) — coming in 19+

Lets you render a subtree in the background, keeping its state, without painting. Useful for: tab interfaces (pre-render the other tab), route prefetch + retain state.

Common pitfalls

  1. Wrapping urgent state in a transition. The input lags. Only mark the consequences as transitions, not the cause.
  2. Side effects inside startTransition. Side effects don't get the interruption / deferral magic — they fire eagerly. Move side effects to useEffect keyed off the deferred state.
  3. Suspense at the wrong level. A Suspense boundary that wraps the whole page falls back to a giant spinner on every refetch — too coarse. Wrap the smallest unit that has its own loading meaning.
  4. useDeferredValue without memo'd children. The whole point is the child re-renders less — if it re-renders anyway due to other props, you've gained nothing.

Senior framing

The interviewer expects:

  1. Naming the four primitives with one-line summaries.
  2. Clear use-cases — what each is for in production code.
  3. Mental model: urgent vs transition.
  4. What's NOT solved — concurrent rendering doesn't speed up slow code.
  5. Interaction with Strict Mode — pure render functions matter more now.
  6. Suspense boundaries placement strategy.

The candidate who lists the APIs is mid. The one who can explain why an input feels janky and how transitions fix it is senior.

Follow-up questions

  • Difference between `useTransition` and `useDeferredValue` in practice?
  • Why does concurrent rendering require pure render functions?
  • How does automatic batching change error handling in async code?
  • When does Suspense at the wrong level hurt UX?

Common mistakes

  • Wrapping urgent state in a transition.
  • Putting side effects inside startTransition.
  • Coarse Suspense boundaries.
  • Assuming concurrent rendering makes slow code fast.

Performance considerations

  • Transitions can interrupt expensive renders — the work isn't wasted if React reuses partial results.
  • useDeferredValue is essentially free unless children re-render.
  • Suspense streaming improves TTFB / FCP for SSR.

Edge cases

  • External stores (Redux, Zustand) need useSyncExternalStore for tearing-free concurrent rendering.
  • Strict Mode double-renders catch impurity bugs.
  • Refs across transitions — the ref may point to the pre-transition DOM.

Real-world examples

  • Tab switchers, search results in a sidebar, route transitions in client routers.
  • Next.js App Router uses transitions for navigation.

Related questions