Hooks Rules & Pitfalls
Hooks are implemented as an ordered sequence of entries attached to the currently rendering fiber. Each hook call advances through that sequence, so React can recover the right state, queue, effect, or memoized value on later renders.
This design exists because hook APIs share one render-time mechanism while exposing different behaviors. The dispatcher selects mount, update, or rerender implementations, and development-only checks detect when a component no longer follows the expected order.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/react-reconciler/src/ReactFiberHooks.js:1004-1073, packages/react-reconciler/src/ReactFiberHooks.js:505-634, packages/react-reconciler/src/ReactFiberHooks.js:3926-3952, packages/react-reconciler/src/ReactFiberHooks.js:3954-3980
Core concepts
Hook call order
A hook call order is the exact sequence of hook invocations that a component performs on every render; React requires that sequence to remain stable.
Sources: packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:675-685
Hook list
A hook list is a singly linked chain of Hook objects stored through currentlyRenderingFiber.memoizedState, with each object pointing to the next hook.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/react-reconciler/src/ReactFiberHooks.js:1004-1073
Dispatcher
A dispatcher is the render-phase table that maps public hook operations such as useState and useEffect to mount, update, or rerender implementations.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:3926-3952, packages/react-reconciler/src/ReactFiberHooks.js:3954-3980
Development validation
Development validation is the extra bookkeeping and warning behavior used to compare hook types, reject invalid nested access, and explain rule violations.
Sources: 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:4028-4035
How call order becomes hook state
On a mount, mountWorkInProgressHook creates a new hook record and either stores it as the fiber’s first hook or appends it to the existing chain.
On an update, updateWorkInProgressHook advances both the current hook pointer and the work-in-progress hook pointer. It reuses an existing work-in-progress entry when possible; otherwise, it clones the matching current entry and appends the clone.
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}The important detail is that the list is positional: the first call consumes the first entry, the second call consumes the second entry, and so on.
Evidence
- renderpackages/react-reconciler/src/ReactFiberHooks.js:505
- mount-dispatcherpackages/react-reconciler/src/ReactFiberHooks.js:3926
- mount-hookpackages/react-reconciler/src/ReactFiberHooks.js:983
- update-hookpackages/react-reconciler/src/ReactFiberHooks.js:1004
- fiber-listpackages/react-reconciler/src/ReactFiberHooks.js:983
- fiber-listpackages/react-reconciler/src/ReactFiberHooks.js:1004
A conditional call shifts every later call against the stored positions. If a stateful hook is encountered where no queue exists, updateReducerImpl throws that hooks are being called conditionally. If an update needs a hook that was not present previously, updateWorkInProgressHook throws “Rendered more hooks than during the previous render.”
If a render stops before consuming the previous list, finishRenderingHooks detects remaining hooks through currentHook.next and throws “Rendered fewer hooks than expected,” including early returns as a possible cause.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/react-reconciler/src/ReactFiberHooks.js:1004-1073, packages/react-reconciler/src/ReactFiberHooks.js:1316-1589, packages/react-reconciler/src/ReactFiberHooks.js:636-753
How the dispatcher changes by render phase
renderWithHooks initializes the render bookkeeping, records the fiber being rendered, and chooses the dispatcher. In production it selects HooksDispatcherOnMount when there is no current fiber or no stored state, and HooksDispatcherOnUpdate otherwise.
The mount and update dispatchers expose the same public operations but route them to different implementations. For example, mount useState uses mountState, while update useState uses updateState; mount useEffect uses mountEffect, while update useEffect uses updateEffect.
| Dispatcher | Phase | Representative storage behavior |
|---|---|---|
HooksDispatcherOnMount | First render | Creates hook entries through mount implementations. |
HooksDispatcherOnUpdate | Later render | Reuses or clones entries through update implementations. |
HooksDispatcherOnRerender | Render-phase rerender | Reprocesses the component while restarting hook pointers. |
ContextOnlyDispatcher | Outside active hook rendering | Becomes the dispatcher after rendering or after a throw. |
Development has additional dispatcher variants. renderWithHooks uses HooksDispatcherOnUpdateInDEV, HooksDispatcherOnMountWithHookTypesInDEV, or HooksDispatcherOnMountInDEV so it can validate ordering even when ordinary stateful-hook detection cannot distinguish mount from update.
Evidence
- render-with-hookspackages/react-reconciler/src/ReactFiberHooks.js:505
- componentpackages/react-reconciler/src/ReactFiberHooks.js:505
- finish-hookspackages/react-reconciler/src/ReactFiberHooks.js:636
- context-dispatcherpackages/react-reconciler/src/ReactFiberHooks.js:636
Sources: 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:636-753, packages/react-reconciler/src/ReactFiberHooks.js:929-941
How violations are caught
The ESLint rule reports a hook called conditionally and states that hooks must run in the exact same order in every component render. It also reports hooks that may execute more than once, such as hooks inside loops.
The package enables react-hooks/rules-of-hooks as an error and react-hooks/exhaustive-deps as a warning in its basic configuration.
Runtime development checks provide a second line of defense. mountHookTypesDev records hook names during the initial render, while updateHookTypesDev compares each later hook name against the recorded position and calls warnOnHookMismatchInDev when they differ.
warnOnHookMismatchInDev emits a previous-render versus next-render table and explains that changing hook order leads to bugs and errors.
Some invalid nesting is guarded separately: warnInvalidHookAccess reports that hooks may only be called at the top level of a React function, not inside useEffect, useMemo, or another built-in hook.
React also protects dispatcher state around failures. After a throw, resetHooksAfterThrow clears the rendering fiber and restores ContextOnlyDispatcher, preventing ordinary hooks from continuing as though a component were still rendering.
Sources: packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:632-645, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:675-685, packages/eslint-plugin-react-hooks/src/index.ts:43-46, 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:4028-4035, packages/react-reconciler/src/ReactFiberHooks.js:929-941
How it connects
The public hook functions resolve the current dispatcher and delegate to it, so the react package stays thin while the reconciler owns mount, update, rerender, and development behavior.
For the broader render context, see Hooks Implementation and Render Phase. The fiber storage that owns memoizedState is described in Fiber Architecture. Static enforcement belongs with ESLint Plugin: react-hooks, while compiler-specific validation is covered by Compiler Validation & Diagnostics.
Sources: packages/react/src/ReactHooks.js:170-185, packages/react-reconciler/src/ReactFiberHooks.js:983-1002, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:675-685
Key takeaways
- Hook state is positional: each render must consume the same ordered hook list.
- Conditional or repeated hook calls can shift entries, produce missing queues, or leave unconsumed hooks.
- Mount, update, rerender, and outside-render phases use different dispatchers.
- Development compares hook types and reports a render-to-render order mismatch.
- ESLint catches many violations before runtime, while the reconciler catches failures during rendering.