How it works
The explanation, with examples you can run.
Queue now, render soon
This toy store (not React's code) shows the general technique: collect updates while the current code runs, and render once in a microtask.
Press Run to see what your code prints, in order.
Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.
The three updates produce one render, after the synchronous code and before the next task. React's batching for a click works the same way from the outside.
What React 19 does, by cause
These timings were observed in Chrome with React 19. They are implementation details that React may change, but they explain the behaviour you see:
- A click, key press or other discrete event. The updates from the handler are rendered and committed in a microtask scheduled when the first update was queued. Microtasks you queued before that still see the old DOM; anything after, including the next frame, sees the new one. Effects caused by the update may also run before the paint, which the
useEffectdocs mention. - A timer, a network response, a message. The updates are batched in the same way (automatic batching, since React 18), but rendered in a task React schedules. Your remaining code and its microtasks see the old DOM, and passive effects usually run after the paint.
- After an
awaitin a handler. Updates made after theawaitare a new batch and a separate render, because the handler's synchronous part has already been processed. - A transition. Rendered in tasks, yielding to the browser regularly, and abandoned if a more urgent update arrives.
When you need the DOM to reflect an update right now (to scroll to a newly added item, or before handing control to a browser API), flushSync from react-dom renders and commits synchronously:
import { flushSync } from "react-dom" declare const listRef: { current: HTMLUListElement | null }declare function setItems(update: (prev: string[]) => string[]): void export function addAndScroll(item: string) { flushSync(() => setItems((prev) => [...prev, item])) listRef.current?.lastElementChild?.scrollIntoView() // the new item is in the DOM}Use it sparingly: it gives up batching and can hurt performance.
Rendering blocks the thread
Rendering is ordinary JavaScript. While React renders an urgent update, nothing else on the main thread can run: no input handlers, no timers, no painting.
Press Run to see what your code prints, in order.
Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.
The timer was due at once, but it runs only after the 200 ms of synchronous work (the exact number printed varies, but is never below 200). A click during a slow render waits in the same way, which shows up as poor responsiveness (INP). The fixes are the ones from earlier topics: render less (memoisation, moving state down), split the work, or mark the slow part as a transition so React can pause between components and let input through.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.