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.
'State scheduling' refers to React's decision of when and at what priority to process a state update.
Before React 18: synchronous everywhere
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.
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.
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.
startTransitionlets 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
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.