Back to the Library

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.”

In This Post

Why Hook Order MattersWhat the Broken Code Looks LikeThe Fix: Move the Hook Above the ConditionThe Other Forms This TakesPractice These Patterns

Practice This Pattern

White Belt

Review Panel Goes Blank on First Click

A review panel that mounts fine but goes blank the instant Write Review is clicked - a useState call inside an if block runs on the second render but not the first, changing the hook count mid-session.

+10 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

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.

Why Hook Order Matters

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.

What the Broken Code Looks Like

export default function ReviewPanel() {
  const [reviewing, setReviewing] = useState(false);
 
  if (reviewing) {
    const [charCount, setCharCount] = useState(0);
 
    return (
      <div>
        <textarea
          placeholder="Write your review..."
          onChange={(e) => setCharCount(e.target.value.length)}
        />
        <p>Characters: {charCount}</p>
        <button onClick={() => setReviewing(false)}>Cancel</button>
      </div>
    );
  }
 
  return (
    <div>
      <p>No review yet.</p>
      <button onClick={() => setReviewing(true)}>Write Review</button>
    </div>
  );
}

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.

The Fix: Move the Hook Above the Condition

export default function ReviewPanel() {
  const [reviewing, setReviewing] = useState(false);
  const [charCount, setCharCount] = useState(0);
 
  if (reviewing) {
    return (
      <div>
        <textarea
          placeholder="Write your review..."
          onChange={(e) => setCharCount(e.target.value.length)}
        />
        <p>Characters: {charCount}</p>
        <button onClick={() => setReviewing(false)}>Cancel</button>
      </div>
    );
  }
 
  return (
    <div>
      <p>No review yet.</p>
      <button onClick={() => setReviewing(true)}>Write Review</button>
    </div>
  );
}

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.

The Other Forms This Takes

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.

Practice These Patterns

"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.