Day 23: State Batching & Concurrent Features (useTransition, useDeferredValue)
Use React 18's automatic batching and Concurrent APIs to keep typing/interaction responsive while expensive re-renders happen in the background.
Study
Concepts
Automatic batching (React 18+)
Batching means React groups multiple `setState` calls that happen within the same event/task into a SINGLE re-render, instead of re-rendering after each call. Before React 18, batching only happened inside React event handlers; calls inside a `setTimeout`, a Promise `.then`, or a native event listener each triggered their own separate render. React 18's automatic batching extends this grouping to ALL of those cases by default, which is a meaningful perf win with zero code changes — and occasionally a surprise if code relied on the old, more eager re-render timing.
`flushSync(fn)` is the escape hatch that forces a synchronous, un-batched render for cases where you genuinely need the DOM updated immediately after a state change (rare — e.g. measuring layout right after a change).
Marking updates as low priority with useTransition
Fiber's interruptible render phase (Day 21) enables priority: some updates (typing in a text box) must feel instant, while others (re-filtering a 10,000-row list based on that text) can wait a few milliseconds without the user noticing. `useTransition` lets you wrap a state update in `startTransition(() => setState(...))`, telling React "this update is not urgent — keep the UI responsive to more urgent updates (like further typing) even if it means delaying or discarding in-progress work on this one." `isPending` reports whether that low-priority work is still in flight, so you can show a subtle loading indicator instead of freezing.
`useDeferredValue(value)` is the sibling API for when you cannot wrap the SETTER in a transition (e.g. the value comes from a prop, not local state) — it returns a version of `value` that lags behind during urgent updates and catches up once the main thread is free, letting you render the expensive part off a "stale-but-cheap" value momentarily.
See It
Visualizations
Visualization
useTransition vs useDeferredValue
| useTransition | useDeferredValue | |
|---|---|---|
| You control | The setState call itself | A value you already receive |
| Typical source | Local state you own | Props or context you don't own |
| Gives you | isPending boolean | A lagging copy of the value |
| Use when | Triggering a low-priority update yourself | Consuming a value that changes urgently elsewhere |
Visualization
Typing while a transition is pending
t=0ms
User types "a"
urgent update: input renders instantly
t=0ms
startTransition schedules the filtered list
marked low priority
t=8ms
User types "b"
urgent — interrupts the in-progress low-priority render
t=40ms
Filtered list finally commits
for the latest value only — intermediate work discarded
Build It
Code Examples
Automatic batching in React 18
function handleClick() {
fetch('/api/data').then(() => {
setCount((c) => c + 1); // React 18: batched...
setFlag((f) => !f); // ...with this — ONE re-render total,
// even though we're inside a Promise callback.
});
}Keeping a search input responsive with useTransition
function SearchableList({ allItems }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(allItems);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // urgent: the input must update instantly
startTransition(() => {
// low priority: can be interrupted by the next keystroke
setFiltered(allItems.filter((i) => i.name.includes(value)));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <span>Updating…</span>}
<ExpensiveList items={filtered} />
</>
);
}Remember
Key Takeaways
- React 18 batches ALL setState calls within one task by default — timers, promises, and native listeners included.
- flushSync is the rare escape hatch to force an immediate, un-batched render.
- useTransition wraps a setState call you own, marking it low-priority and giving you an isPending flag.
- useDeferredValue lags a value you don't control the setter for, letting urgent renders skip ahead of it.
- Both APIs rely entirely on Fiber's interruptible render phase — they are impossible without the Day 21 architecture.
Do It
Practice
- 1Build a slow-rendering list (1,000+ items with an artificial render delay) filtered by a text input; feel the lag, then fix it with useTransition.
- 2Reproduce the pre-React-18 vs React-18 batching difference by counting renders inside a setTimeout callback with two setState calls.
- 3Swap useTransition for useDeferredValue in the search example by lifting query into a parent and passing it down as a prop.