Skip to solution
hardFrontend

What do `startTransition` and `useTransition` actually do under the hood?

520 views
01

Understand the problem

Question presented to candidate: "You wrap a state update in startTransition. What actually changes about how React renders it?"

What a strong answer should cover:

  • A transition marks an update non-urgent. It does not make the work faster — it changes when and at what priority the work is done.
  • The observable effect is a split: the urgent update commits on its own first, and the transition update commits in a second render.
  • That second render is interruptible. If a new urgent update arrives while it is in progress, React abandons the partial work and starts over with the newer state.
  • The old UI stays on screen while the transition renders — no fallback, no blank space, which is the difference from a plain Suspense boundary.
  • useTransition gives you an isPending boolean; startTransition imported from React does not, and can be called outside a component.
  • Never wrap the controlled value of an input in a transition — the input must update urgently or typing feels broken.
  • It changes priority, not cost. If a render takes 300ms it still takes 300ms; it just no longer blocks the keystroke.

Clarifying questions expected:

  • "Which part of the update is the user waiting on directly?" — that part stays urgent.
  • "Is the slowness the render itself, or fetching data?" — a transition helps the first, not the second.

Code / implementation expected: Optional. Showing which setState goes inside the transition and which stays outside is the substance.

reactconcurrentperformance
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes hooks and re-render basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every render count and ordering below was **executed against Reac

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same slow filter, with and without a transition
Run Playground
import { useState, useTransition } from "react";

const ROWS = Array.from({ length: 2000 }, (_, i) => "Item " + i);

function slowFilter(q) {
  // Deliberately expensive so the difference is visible, not theoretical.
  const out = [];
  for (const r of ROWS) {
    let acc = 0;
    for (let i = 0; i < 200; i++) acc += i % 7;
    if (acc >= 0 && r.includes(q)) out.push(r);
  }
  return out;
}

// ❌ Both updates batch into ONE render, so the typed character cannot appear
//    until the 2000-row filter has finished. The input feels stuck.
function WithoutTransition() {
  const [text, setText] = useState("");
  const [result, setResult] = useState({ query: "", rows: ROWS.length });
  return (
    <Panel
      title="❌ no transition"
      value={text}
      pending={false}
      result={result}
      onChange={(v) => {
        setText(v);
        setResult({ query: v, rows: slowFilter(v).length });
      }}
    />
  );
}

// ✅ Two renders per keystroke. The first shows the typed character with the
//    OLD result still on screen and isPending true; the second commits the
//    filtered rows. Watch the "showing results for" line lag behind.
function WithTransition() {
  const [isPending, startTransition] = useTransition();
  const [text, setText] = useState("");
  const [result, setResult] = useState({ query: "", rows: ROWS.length });
  return (
    <Panel
      title="✅ transition"
      value={text}
      pending={isPending}
      result={result}
      onChange={(v) => {
        setText(v);                                     // urgent: the input
        startTransition(() =>                           // non-urgent: the list
          setResult({ query: v, rows: slowFilter(v).length }));
      }}
    />
  );
}

function Panel({ title, value, pending, result, onChange }) {
  const stale = value !== result.query;
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 }}>
      <strong style={{ fontSize: 13 }}>{title}</strong>
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder="type here"
        style={{ width: "100%", margin: "6px 0" }}
      />
      <div style={{ fontSize: 13, opacity: pending ? 0.5 : 1 }}>
        showing <strong>{result.rows}</strong> rows for{" "}
        <code>{JSON.stringify(result.query)}</code>
        {pending && <em> — updating…</em>}
        {stale && !pending && <em> — stale</em>}
      </div>
    </div>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <WithoutTransition />
      <WithTransition />

      <p style={{ fontSize: 13, color: "#666" }}>
        Type quickly in both. In the first, the character itself does not
        appear until the filter finishes — one render, and you wait for it. In
        the second the character lands immediately while the results line still
        shows the PREVIOUS query, dimmed, until the second render commits.
        That visible lag is the transition doing its job.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 100 of 119 decoded in the React.js track. One more won't hurt.

Back to track