Back to React
React
easy
mid

What is state scheduling in React and how does it work?

State scheduling is how React decides WHEN to actually apply a state update and re-render. React 18 introduced automatic batching (multiple setState calls in a single tick collapse to one render) and priority-based scheduling: urgent updates (clicks, input) run synchronously; non-urgent updates wrapped in startTransition can be deferred or interrupted by higher-priority work.

7 min read·~5 min to think through

'State scheduling' refers to React's decision of when and at what priority to process a state update.

Before React 18: synchronous everywhere

tsx
function onClick() {
  setA(1);
  setB(2); // pre-18 inside async callback: two renders
}

Inside React event handlers, updates batched. Inside setTimeout, fetch .then, or async functions, every setState triggered a render.

React 18: automatic batching everywhere

Multiple state updates in any context (event handler, promise, setTimeout, native event) collapse into a single render.

tsx
fetch('/api').then(() => {
  setA(1);
  setB(2); // ONE render in React 18
});

Priority lanes

React internally tags updates with a priority lane:

  • Sync / Discrete — clicks, keypress, focus. Runs immediately.
  • Continuous — drag, scroll, mousemove. Slightly lower.
  • Default — most other updates.
  • Transition — updates inside startTransition. Interruptible.
  • Idle — bottom of the barrel.
tsx
const [pending, startTransition] = useTransition();

function onChange(e) {
  setQuery(e.target.value);            // urgent — input must feel snappy
  startTransition(() => {
    setResults(filter(e.target.value)); // can be interrupted by next keystroke
  });
}

Why scheduling matters

  • A slow filter on keystroke would block input. startTransition lets React commit the input update first, then work on results in the background.
  • If the user types again before results commit, React throws away the in-flight render and starts over with the newer input — no wasted screen flashes.

useDeferredValue — declarative version

tsx
const deferred = useDeferredValue(query);
const results = useMemo(() => filter(deferred), [deferred]);

React lets query change immediately but keeps deferred lagging during expensive work.

What scheduling does NOT change

  • Function components still run top-to-bottom in a render.
  • State setters are still 'fire-and-forget' for the current render — you read the new value next render.

Mental model

Think of React as a queue: each setState enqueues an update with a priority. The scheduler decides whether to flush synchronously (urgent) or yield to the browser and resume later (transition).

Follow-up questions

  • What's the difference between useTransition and useDeferredValue?
  • When does React skip a render that's already in progress?
  • How does automatic batching interact with flushSync?

Common mistakes

  • Wrapping the URGENT update in startTransition — input lags because you deprioritized the wrong thing.
  • Expecting state to be updated synchronously after setState — it's queued.
  • Using flushSync to force sync rendering inside a transition, defeating the point.

Performance considerations

  • Transitions cost extra CPU because React may render twice (urgent commit, then transition commit). Worth it when the work is heavy enough that a single sync render would drop frames. For trivial updates, skip transitions — overhead beats benefit.

Edge cases

  • If a transition update reads a value that an urgent update changed, React replays the transition.
  • Effects from a discarded render don't run.
  • Some updates (e.g. inside Suspense fallback) follow extra rules around revealing/hiding.

Real-world examples

  • Search-as-you-type with an expensive filter, tab switches that need to render a big new view, route transitions where you want the URL bar to feel instant. Next.js App Router uses transitions internally for client-side navigation.

Senior engineer discussion

Senior signal: knowing this is fundamentally about user-perceived latency, not raw throughput. Concurrent React trades a small CPU overhead for the ability to keep input responsive under heavy renders. Pair with profiling — don't transition by default; transition when you measure jank.

Related questions