useState & useReducer
useState and useReducer are two public entry points into the reconciler’s hook state machinery. useState supplies a built-in reducer, while useReducer accepts the reducer supplied by the caller.
They exist as separate APIs because callers need different ways to describe state changes, but they share queueing, lane assignment, scheduling, and render-time processing. This common path lets both hooks participate in the same update model.
Sources: packages/react/src/ReactHooks.js:68-79, packages/react-reconciler/src/ReactFiberHooks.js:1265-1268, packages/react-reconciler/src/ReactFiberHooks.js:1962-1966, packages/react-reconciler/src/ReactFiberHooks.js:1270-1305, packages/react-reconciler/src/ReactFiberHooks.js:1920-1946, packages/react-reconciler/src/ReactFiberHooks.js:3626-3654
Core concepts
Hook cell
A hook cell is the per-hook record attached to the rendering fiber; it stores the current state, base state, base queue, update queue, and link to the next hook.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/react-reconciler/src/ReactFiberHooks.js:1052-1069
Update queue
An update queue is the shared per-hook state used to retain pending updates, the dispatch function, the last reducer, and the last rendered state.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:1290-1303
Eager bailout
An eager bailout is the fast path that computes the next state before rendering and avoids scheduling a re-render when that state is identical to the current state.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:3675-3706
How both hooks use the same queue model
On mount, mountStateImpl creates a hook and an UpdateQueue whose lastRenderedReducer is basicStateReducer; mountState then binds dispatchSetState to that queue.
mountReducer creates the same essential queue shape—pending, lanes, dispatch, lastRenderedReducer, and lastRenderedState—but stores the caller’s reducer and binds dispatchReducerAction.
The distinction between the APIs is therefore the reducer: basicStateReducer treats a function action as an updater function and otherwise uses the action as the next state, while useReducer uses the supplied reducer.
On updates, updateState is a direct call to updateReducer with basicStateReducer. That delegation is the clearest proof that ordinary state updates and reducer updates share the same update-processing implementation.
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}The important detail is that useState changes the reducer supplied to the shared machinery, not the queue-processing model itself.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:1920-1945, packages/react-reconciler/src/ReactFiberHooks.js:1948-1959, packages/react-reconciler/src/ReactFiberHooks.js:1270-1304, packages/react-reconciler/src/ReactFiberHooks.js:1265-1268, packages/react-reconciler/src/ReactFiberHooks.js:1962-1966
How updates are staged and processed
A hook dispatch creates an update containing its lane, action, eager-state metadata, and link field. dispatchReducerAction requests a lane, handles render-phase updates separately, and otherwise sends the update through enqueueConcurrentHookUpdate.
enqueueConcurrentHookUpdate passes the fiber, queue, update, and lane to enqueueUpdate, then finds the root for the updated fiber.
enqueueUpdate does not immediately splice the update into the hook queue. It appends the fiber, queue, update, and lane to concurrentQueues, merges the lane into concurrentlyUpdatedLanes, and marks both the fiber and its alternate.
Later, finishQueueingConcurrentUpdates drains those staged entries. For each queue, it links the update into a circular pending list; for non-empty lanes it also marks the update lane from the fiber to the root.
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;This circular list is why multiple updates can wait in the same hook queue before render processes them.
During updateReducerImpl, pending updates are merged into the base queue, queue.pending is cleared, and the reducer walks the resulting circular list to compute the new state according to the active render lanes.
The root is found by walking the fiber’s return path because the update queue has no root backpointer.
The call order below shows the normal dispatch path and the later queue-linking step.
Evidence
Sources: packages/react-reconciler/src/ReactFiberHooks.js:3583-3623, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:115-125, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:90-112, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:50-84, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:67-82, packages/react-reconciler/src/ReactFiberHooks.js:1316-1373, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:252-275
When an update bails out eagerly
dispatchSetStateInternal first creates the update and checks whether it is a render-phase update. For a normal update, it considers eager evaluation only when both the fiber and its alternate have no scheduled lanes, meaning the queue is currently empty from this path’s perspective.
If queue.lastRenderedReducer exists, React applies it immediately to queue.lastRenderedState and stores the result on the update as eagerState. If the result is identical under is, React calls enqueueConcurrentHookUpdateAndEagerlyBailout and returns without scheduling ordinary work.
The bailout is not deletion: the update is still queued with NoLane so a later higher-priority render can rebase it if necessary.
Because no work is scheduled by the bailout itself, enqueueConcurrentHookUpdateAndEagerlyBailout immediately calls finishQueueingConcurrentUpdates when React is not already rendering. This prevents the queued update from leaking indefinitely.
If eager evaluation produces a different state, the normal path enqueues the update, schedules the root, and entangles transition work when applicable.
Therefore, calling a setter skips a re-render entirely only when the queue is eligible for eager evaluation, the reducer can be run eagerly, and the computed state is identical to the last rendered state.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:3656-3683, packages/react-reconciler/src/ReactFiberHooks.js:3683-3707, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:127-143, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:140-150, packages/react-reconciler/src/ReactFiberHooks.js:3718-3723, packages/react-reconciler/src/ReactFiberHooks.js:3675-3706
How multiple calls are batched
At an event boundary, batchedUpdates marks that React is inside an event handler, delegates to batchedUpdatesImpl, and restores the boundary state afterward. Nested calls reuse the active batch instead of opening another one.
Each state dispatch still creates its own update, but the concurrent queue staging area records those updates before finishQueueingConcurrentUpdates links them into the hook’s circular pending queue.
This means several setState calls in one handler are represented as several ordered updates in one queue. During the next reducer pass, updateReducerImpl walks that queue and applies the updates whose lanes are included in the current render.
Sources: packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js:46-59, packages/react-reconciler/src/ReactFiberHooks.js:3583-3623, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:90-112, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:50-82, packages/react-reconciler/src/ReactFiberHooks.js:1316-1373
How it connects
The public useState and useReducer functions resolve the current dispatcher and forward their arguments to it; the reconciler dispatcher then selects mount, update, or rerender implementations.
Hook ordering is checked in development because dispatchers record hook types on mount and compare them during updates. A mismatch calls warnOnHookMismatchInDev.
The hook list and dispatcher lifecycle are part of Hooks Implementation, while lane selection and root scheduling continue in Lanes & Priority and Work Loop & Scheduling. The fiber fields that carry lanes and hook state are described in Fiber Architecture.
Sources: packages/react/src/ReactHooks.js:68-79, packages/react-reconciler/src/ReactFiberHooks.js:4102-4116, packages/react-reconciler/src/ReactFiberHooks.js:4430-4444, packages/react-reconciler/src/ReactFiberHooks.js:311-321, packages/react-reconciler/src/ReactFiberHooks.js:323-334
Key takeaways
useStateisuseReducer-style queue processing withbasicStateReducer.- Hook updates are staged, then linked into a circular pending queue.
- An eager bailout skips scheduling when the eagerly computed state equals the last rendered state.
- Event batching lets multiple dispatches accumulate before queue processing.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:1265-1268, packages/react-reconciler/src/ReactFiberHooks.js:1962-1966, packages/react-reconciler/src/ReactFiberConcurrentUpdates.js:50-84, packages/react-reconciler/src/ReactFiberHooks.js:3683-3707, packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js:46-59