C.W.K.
Stream
Lesson 03 of 06 · published

Scheduling Intuition — Priority, Batching, Yielding

~10 min · scheduling, concurrent, internals

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
You don't need to know React's scheduler internals to use it well. You do need a model of what 'high priority' and 'low priority' actually mean — otherwise the hooks just feel like magic.

Three priority levels (mental model)

  • Synchronous / urgent — input events, controlled-input updates, click responses. These render immediately. The user sees the change in the same tick.
  • Transition / non-urgent — anything inside startTransition. React renders these without blocking the main thread; the browser stays responsive.
  • Suspense / async — anything that suspends. React waits for the promise, keeps showing previous content (with a transition) or the fallback (without).

You don't pick the priority manually — you pick the hook (useState alone, startTransition, use + Suspense) and the priority follows.

Batching

React 19 batches all state updates inside the same event handler, async function, or callback. setA(1); setB(2); in a click handler triggers one render. This is automatic — you don't need flushSync for the common case.

Yielding

During a transition render, React periodically yields to the browser. If the user types or clicks, React processes the urgent event first, then resumes the transition. This is what keeps the app responsive during heavy renders.

What this means for your code

Most of the time: nothing. React handles it. The places it matters:

  1. Mark heavy state updates with useTransition (lesson 1).
  2. Use useDeferredValue for props you don't own (lesson 2).
  3. Trust the React Compiler (next lesson) to memoize wherever it helps.
  4. Don't reach for flushSync — almost never the right answer in 2026.
Scheduling is React's job; semantics are yours. You tell React what you mean (this update is urgent, that one isn't); React decides when to actually paint each one. Trying to control timing manually fights the scheduler instead of using it.

Code

What batching looks like in practice·tsx
function MultiState() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  const [c, setC] = useState(0);

  function onClick() {
    // All three updates batch into one render — React 19 automatic.
    setA((x) => x + 1);
    setB((x) => x + 1);
    setC((x) => x + 1);
  }

  console.log("Render"); // logs once per click, not three times
  return <button onClick={onClick}>Click ({a + b + c})</button>;
}

External links

Exercise

Build a component with three useState calls. In a click handler, call all three setters and add a console.log('render') in the component body. Click once and verify only one log appears (batched). Then break batching with flushSync between two of the setters — observe two logs. Restore batching and notice how natural it feels.
Hint
Import flushSync from react-dom. Once you see the extra render, you'll understand why batching is the default and flushSync is the exception.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.