Hooks Implementation
Hooks are implemented as a render-time dispatcher: public hook functions resolve the current dispatcher, while the reconciler selects the dispatcher appropriate to the component’s mount, update, or rerender pass.
This indirection exists because the same call, such as useState, must create state on the first render but read and update existing state later. The implementation also needs a safe mode outside rendering, plus development checks for hook order and invalid nesting.
Sources: packages/react/src/ReactHooks.js:24-38, packages/react-reconciler/src/ReactFiberHooks.js:505-634, packages/react-reconciler/src/ReactFiberHooks.js:3926-3952, packages/react-reconciler/src/ReactFiberHooks.js:3954-3980, packages/react-reconciler/src/ReactFiberHooks.js:3982-4008, packages/react-reconciler/src/ReactFiberHooks.js:445-454, packages/react-reconciler/src/ReactFiberHooks.js:311-321, packages/react-reconciler/src/ReactFiberHooks.js:323-334
Core concepts
Dispatcher
A dispatcher is the render-phase implementation table that public hooks call through; renderWithHooks installs HooksDispatcherOnMount, HooksDispatcherOnUpdate, or a development variant before invoking the component.
| Dispatcher | Plain-language role |
|---|---|
HooksDispatcherOnMount | Creates hook records and initializes their state. |
HooksDispatcherOnUpdate | Reuses the previous hook records and processes updates. |
HooksDispatcherOnRerender | Replays hooks after a render-phase update. |
ContextOnlyDispatcher | Guards the period when normal hook rendering is not active. |
These dispatcher variants are the central mechanism that lets one public hook name select different mount, update, rerender, or invalid-call behavior.
Sources: packages/react/src/ReactHooks.js:24-38, packages/react-reconciler/src/ReactFiberHooks.js:505-634, packages/react-reconciler/src/ReactFiberHooks.js:3898-3924, packages/react-reconciler/src/ReactFiberHooks.js:3926-3952, packages/react-reconciler/src/ReactFiberHooks.js:3954-3980, packages/react-reconciler/src/ReactFiberHooks.js:3982-4008
Rendering context
The rendering context is the temporary module state that identifies the fiber and the current hook positions, including currentlyRenderingFiber, currentHook, and workInProgressHook.
renderWithHooks sets currentlyRenderingFiber, records the render lanes, clears the work-in-progress state, and chooses the dispatcher before calling the component.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:264, packages/react-reconciler/src/ReactFiberHooks.js:270, packages/react-reconciler/src/ReactFiberHooks.js:271, packages/react-reconciler/src/ReactFiberHooks.js:513-566
Hook list
A hook list is the linked list stored in currentlyRenderingFiber.memoizedState; each hook record contains state, queues, and a next pointer.
On mount, mountWorkInProgressHook appends a new record. On update, updateWorkInProgressHook advances through the current list or clones the matching current record into the work-in-progress list.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/react-reconciler/src/ReactFiberHooks.js:1004-1073
Hook position
A hook position is the call-order slot that connects a component’s current hook call to its stored record. updateWorkInProgressHook obtains the next current hook from currentHook.next and the next work-in-progress hook from workInProgressHook.next, so changing call order changes which record a hook reads.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:1004-1073
How the dispatcher changes behavior
The dispatcher is selected from whether the fiber has a current alternate and whether its memoizedState contains stateful hooks. In production, a missing current fiber or missing memoized state selects HooksDispatcherOnMount; otherwise React selects HooksDispatcherOnUpdate.
Development adds dispatcher variants that retain the same mount/update distinction while validating hook types. If a component is updating without stateful hooks, HooksDispatcherOnMountWithHookTypesInDEV preserves production behavior while still checking hook order.
| Phase | Dispatcher choice | Why it differs |
|---|---|---|
| Initial mount | HooksDispatcherOnMount | There is no previous hook list to read. |
| Normal update | HooksDispatcherOnUpdate | Existing hook records and queues must be reused. |
| Render-phase rerender | HooksDispatcherOnRerender | A state update during render requires another pass over the same work-in-progress hooks. |
The rerender path resets the hook cursors, installs HooksDispatcherOnRerender, invokes the component again, and repeats while render-phase updates remain pending.
ReactSharedInternals.H = __DEV__
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;This assignment is the explicit dispatcher swap for a render-phase rerender; the loop around it is bounded by RE_RENDER_LIMIT to prevent infinite rerendering.
A suspended component can also be replayed. replaySuspendedComponentWithHooks calls renderWithHooksAgain and then finishRenderingHooks, allowing the component to resume with the hook machinery still aligned.
This sequence shows the main render hand-off and the separate mount/update hook implementations that consume the selected dispatcher.
Evidence
- render-with-hookspackages/react-reconciler/src/ReactFiberHooks.js:505
- componentpackages/react-reconciler/src/ReactFiberHooks.js:599
- mount-statepackages/react-reconciler/src/ReactFiberHooks.js:1948
- update-statepackages/react-reconciler/src/ReactFiberHooks.js:1962
- mount-hookpackages/react-reconciler/src/ReactFiberHooks.js:983
- update-hookpackages/react-reconciler/src/ReactFiberHooks.js:1004
- finish-hookspackages/react-reconciler/src/ReactFiberHooks.js:636
Sources: packages/react-reconciler/src/ReactFiberHooks.js:546-567, packages/react-reconciler/src/ReactFiberHooks.js:549-560, packages/react-reconciler/src/ReactFiberHooks.js:791-857, packages/react-reconciler/src/ReactFiberHooks.js:821-855, packages/react-reconciler/src/ReactFiberHooks.js:755-789
How hook calls map to stored state
On mount, mountWorkInProgressHook creates a record with memoizedState, base state, an update queue, and next; the first record becomes the fiber’s memoizedState, and later records are appended.
A state hook uses that record to store its initial value and queue. mountState delegates initialization to mountStateImpl, then binds dispatchSetState to the currently rendering fiber and queue.
On update, updateWorkInProgressHook walks the current and work-in-progress lists in lockstep. It reuses an existing work-in-progress record when available; otherwise it clones the corresponding current record, including memoizedState, base queues, and the update queue.
The same positional rule applies to other hooks. For example, updateRef reads the record returned by updateWorkInProgressHook, while updateCallback and updateMemo read the stored value and dependency array from that record.
React therefore does not identify a hook by a variable name. It identifies it by the order in which the dispatcher receives calls during a component render. Development records hook names during mount and compares each later name with the corresponding recorded position. A mismatch invokes warnOnHookMismatchInDev.
If an update needs more hook records than the previous render supplied, updateWorkInProgressHook throws “Rendered more hooks than during the previous render.” If the render finishes with unvisited current records, finishRenderingHooks throws “Rendered fewer hooks than expected.”
Sources: packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/react-reconciler/src/ReactFiberHooks.js:1948-1960, packages/react-reconciler/src/ReactFiberHooks.js:1004-1073, packages/react-reconciler/src/ReactFiberHooks.js:2635-2638, packages/react-reconciler/src/ReactFiberHooks.js:2930-2942, packages/react-reconciler/src/ReactFiberHooks.js:2963-2988, packages/react-reconciler/src/ReactFiberHooks.js:311-321, packages/react-reconciler/src/ReactFiberHooks.js:323-334, packages/react-reconciler/src/ReactFiberHooks.js:351-396, packages/react-reconciler/src/ReactFiberHooks.js:1037-1049, packages/react-reconciler/src/ReactFiberHooks.js:664-712
Why calls outside render fail
Public hooks first read ReactSharedInternals.H through resolveDispatcher. In development, a null dispatcher produces the invalid-hook-call diagnostic; the implementation then relies on the dispatcher access to fail when no render dispatcher is available.
After a render completes, finishRenderingHooks resets ReactSharedInternals.H to ContextOnlyDispatcher, clears currentlyRenderingFiber, and clears both hook cursors. The same guarded dispatcher is installed after a throw by resetHooksAfterThrow, specifically to prevent hooks from being called outside a component render.
The failure is therefore mechanical: outside the render window there is no active fiber or hook cursor, and the active dispatcher is not the mount/update implementation. The invalid dispatcher path uses throwInvalidHookError, while development-only invalid nested-hook dispatchers also warn when hooks are called inside built-in hooks rather than at the component’s top level.
Sources: packages/react/src/ReactHooks.js:24-38, packages/react-reconciler/src/ReactFiberHooks.js:658-672, packages/react-reconciler/src/ReactFiberHooks.js:929-941, packages/react-reconciler/src/ReactFiberHooks.js:445-454, packages/react-reconciler/src/ReactFiberHooks.js:4028-4035
How it connects
The reconciler invokes renderWithHooks while building a function component’s work, so hook state is attached to the fiber rather than kept in the public react package. The public package supplies the dispatcher lookup and forwards each exported hook call to the selected implementation.
Hook state queues then hand updates back to the reconciler. dispatchSetStateInternal distinguishes render-phase updates from ordinary updates; ordinary updates enter a concurrent hook queue and call scheduleUpdateOnFiber. The hook list and fiber double-buffer are described in Fiber Architecture, while the surrounding render process is covered in Render Phase and update priority in Lanes & Priority.
The user-facing API surface is listed in Public API Reference. The invariant that hooks must preserve call order is also enforced statically by ESLint Plugin: react-hooks, while runtime mismatch diagnostics are implemented here.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:505-634, packages/react/src/ReactHooks.js:188-207, packages/react-reconciler/src/ReactFiberHooks.js:3656-3726, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:632-644, packages/react-reconciler/src/ReactFiberHooks.js:323-334
Key takeaways
- The dispatcher swaps mount, update, and rerender implementations through ReactSharedInternals.H.
- Hook state is a linked list on the fiber, matched by call position.
- Mount implementations create records; update implementations clone or reuse corresponding records.
- Render completion restores
ContextOnlyDispatcherand clears render cursors. - Development compares recorded hook names and reports changed hook order.