facebook/reactMITd083ec1Report / request removal

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.

Ordered hook storage — How does each render find the state for each hook call?

Evidence

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.

DispatcherPhaseRepresentative storage behavior
HooksDispatcherOnMountFirst renderCreates hook entries through mount implementations.
HooksDispatcherOnUpdateLater renderReuses or clones entries through update implementations.
HooksDispatcherOnRerenderRender-phase rerenderReprocesses the component while restarting hook pointers.
ContextOnlyDispatcherOutside active hook renderingBecomes 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.

Render dispatcher selection — What happens before and after a component's hooks run?

Evidence

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.

Want this for your repos?

Try Angada AI Wiki