Topics in this subject
React 3 min read Updated 5 Aug 2026

Concurrent Features

React 18 interruptible rendering with useTransition, useDeferredValue, and startTransition to keep the UI responsive.

The core idea#

React 18 introduced concurrent rendering: rendering work is now interruptible. React can start rendering an update, pause to handle a more urgent one (like typing), then resume — instead of blocking the main thread until a big render finishes. You opt in by marking some updates as non-urgent (transitions). 🎯

Two update priorities:

Priority Examples Behaviour
Urgent Typing, clicks, hovers Applied immediately, never interrupted
Transition Filtering a big list, tab switch, search results Rendered in the background; interruptible

useTransition#

Returns [isPending, startTransition]. Wrap the state update that triggers heavy work so it doesn't block the urgent input update.

function Search({ allItems }) {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState(allItems);
  const [isPending, startTransition] = useTransition();

  function onChange(e) {
    setQuery(e.target.value);              // urgent: input stays snappy
    startTransition(() => {                // non-urgent: heavy filter
      setResults(allItems.filter((i) => i.name.includes(e.target.value)));
    });
  }

  return (
    <>
      <input value={query} onChange={onChange} />
      {isPending && <Spinner />}
      <List items={results} />
    </>
  );
}

useDeferredValue#

Defers a value instead of wrapping an updater — useful when the expensive consumer is a child and you don't own the setState. React keeps showing the old value while re-rendering with the new one in the background.

function App({ text }) {
  const deferredText = useDeferredValue(text);   // lags behind during heavy renders
  return <SlowList text={deferredText} />;        // memoize SlowList to skip work
}

⚠️ useDeferredValue only helps if the heavy child is memoized (React.memo); otherwise it re-renders every time anyway.

startTransition (standalone)#

The non-Hook version for use outside components (no isPending), e.g. in a router or event handler.

import { startTransition } from "react";
startTransition(() => setPage("dashboard"));

When to use: keep an input responsive while a large derived list, chart, or tab re-renders. Do not wrap the state that drives a controlled input itself in a transition — the input value must be urgent, or typing feels laggy.

flowchart TD
  A["User types in input"] --> B["setQuery — URGENT"]
  B --> C["Input updates immediately"]
  A --> D["startTransition(setResults) — non-urgent"]
  D --> E["Heavy list renders in background"]
  E --> F{"New keystroke arrives?"}
  F -->|"Yes"| G["React interrupts, restarts with fresh input"]
  F -->|"No"| H["Commit list, isPending -> false"]

Interview Q&A#

Q1. What does "concurrent rendering" actually mean? Rendering is interruptible. React can begin a render, yield to handle higher-priority updates (like user input), and resume or discard the in-progress render. It's a capability, not a mode you turn on globally — features like transitions opt into it.

Q2. useTransition vs useDeferredValue? useTransition wraps the state updater you control and gives you isPending. useDeferredValue wraps a value you receive (e.g. a prop) when you can't wrap its setter. Both downgrade work to non-urgent; pick based on whether you own the update.

Q3. Why shouldn't you wrap a controlled input's own value update in a transition? The input value must reflect keystrokes instantly. Transitions are interruptible and may lag, making typing feel broken. Keep setQuery urgent; mark only the expensive derived update as a transition.

Q4. Does useDeferredValue make a slow component fast? No — it defers when the component receives the new value, letting the old UI stay interactive. The heavy child must be memoized so it can skip re-rendering with the stale value; otherwise there's no benefit.