React Rendered More Hooks Than During the Previous Render
5 min read
“The component mounts cleanly. One click later, the whole area goes blank. React threw an error you didn't expect, about hooks you wrote yourself, because the number of hooks it counted just changed.”
The page loads. The component renders. Everything looks fine.
You click a button. The component area goes blank. No error boundary message,
no fallback - just empty space where your UI was. And in the console:
React has detected a change in the order of Hooks called by ReviewPanel. This will lead to bugs and errors if not fixed. Rendered more hooks than during the previous render.
React is not being vague. This error message is a precise description of
exactly what happened, line by line in your code.
React tracks state across renders using an internal list - a simple ordered
array attached to each component instance. Every useState, useEffect,
useCallback, and other hook call appends to this list in the order the
calls are made, top to bottom, every render.
There are no names. No strings. No keys. Hook 1, Hook 2, Hook 3 - that is
all React knows. On the first render, the list is built. On every render
after that, React walks the same list in the same order, handing each call
back the state it stored at that position.
This means the list must be identical in length and order on every render.
Not roughly similar. Identical. If render one registers two hooks and render
two registers three, the third hook has no stored state to receive - and
more importantly, every hook after the inserted one is now misaligned with
the stored values that belong to a completely different position. React
detects the length change and throws before anything further can go wrong.
On the first render, reviewing is false. The if block is skipped
entirely. React registers one hook: useState(false). The list has one
entry. Everything mounts fine.
Click "Write Review." setReviewing(true) fires. React re-renders.
reviewing is now true. The if block runs this time - and inside it,
a second useState(0) call is made. React walks its list expecting one
hook, finds two, and throws.
Symptom
The component renders and looks completely normal on load. The error
doesn't happen until the first action that changes the condition - here,
the first click. After that, the component area goes blank and cannot
recover without a page refresh.
The timing is what makes this hard to catch. Because the first render looks
fine, a quick check of "does it load?" passes. The bug only surfaces on the
first meaningful interaction.
One line moved - const [charCount, setCharCount] = useState(0) now sits
above the if block, unconditionally. React registers two hooks on every
render, in the same order, every time. The list is stable. The error
disappears.
charCount is now initialized even when reviewing is false - which
means on those renders, it's just an unused variable. That is completely
harmless. A hook that is called but whose return value is ignored is still
correctly tracked in React's list, still has its state preserved across
renders, and produces no side effects.
The Sensei's Hint
The instinct to scope a variable to the block where it is used is correct
JavaScript practice everywhere except inside a React component's hook calls.
Move the hook unconditionally to the top, then use the value only where
it's needed. The call must be unconditional; the usage can be wherever
it makes sense.
Hooks inside if blocks are the most common version, but the same error
appears any time the hook count changes between renders:
After an early return. If you do if (!data) return null before a
hook call, the renders where data is falsy register fewer hooks than the
renders where it's truthy. Move all hook calls above any early return.
Inside a loop.for and while loops with a variable number of
iterations produce a different number of hook calls per render. React has
no way to reconcile this.
Inside a nested function. Calling a hook inside a callback, event
handler, or helper function doesn't run it at the top level of the
component - React only tracks hooks that run unconditionally at the top
level of the component body.
All of these are fixed the same way: move the hook call to the
unconditional top-level section. Apply conditional logic inside the
hook's arguments, its callback body, or after the return value - never
to whether the hook itself is called.
Constraints
ESLint's react-hooks/rules-of-hooks rule catches every one of these
patterns statically, before you ever run the code. If you're not running
that rule, this error is much harder to prevent - any hook inside an if,
a loop, a nested function, or after an early return will flag it
immediately.
"Review Panel Goes Blank on First Click" is the cleanest version of this
bug - one hook inside one if block, one click to trigger the crash. Work
through it by predicting what the hook list looks like on render one versus
render two, then move the hook and verify the list is now identical both
times. The fix being one line is the point: the code isn't wrong because of
what it does, only because of where the call lives.