Skip to the solution
HardFrontend7 min read

What underlying mechanism makes the Rules of Hooks mandatory?

659 views

The problem

Question presented to candidate: "The Rules of Hooks say call them at the top level and never conditionally. What is the actual implementation reason?"

What a strong answer should cover:

  • React stores a component hook state in an ordered list on its fiber, and matches each call to its slot by position, not by name.
  • There is no identifier available: useState(0) gives React nothing to key on. The index in the call sequence is the identity.
  • A conditional call shifts every later hook onto the wrong slot β€” so a useState can land on a useEffect slot, reading another hook's data.
  • React detects the count mismatch and throws: "Rendered fewer hooks than expected. This may be caused by an accidental early return statement."
  • The same reasoning covers early returns, loops with variable counts, and hooks inside nested functions or conditions.
  • Why the design: it keeps hooks a plain function call with no registration, no keys, and no boilerplate β€” an explicit trade of flexibility for ergonomics.
  • The tooling angle: this constraint is what makes hooks statically analysable, which is why the ESLint rule can verify them and the React Compiler can memoise safely.
  • The workaround: put the condition inside the hook, or extract a component.
  • use() is the deliberate exception β€” it may be called conditionally because it does not own a state slot.

Clarifying questions expected:

  • "Do you want the failure mode, or why React chose positional matching over named slots?"

Code / implementation expected: Optional. A minimal hook implementation using an array and a cursor is the clearest way to show it.

  • react
  • hooks
  • internals

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 10 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA 15-line reimplementation of hook slots, and the rule it forces
import { useState } from "react";

// ── A miniature of React's mechanism: an array of slots and a cursor. ──────
// This is the whole reason for the Rules of Hooks.
const slots = [];
let cursor = 0;
let rerender = () => {};

function miniUseState(initial) {
  const i = cursor++;                          // THIS position is the identity
  if (!(i in slots)) slots[i] = initial;
  const set = (v) => { slots[i] = v; rerender(); };
  return [slots[i], set, i];
}

function runMiniComponent(includeMiddle) {
  cursor = 0;                                  // reset before every "render"
  const trace = [];
  const [a, , ia] = miniUseState("name");
  trace.push({ hook: "useState(name)", slot: ia, value: a });

  if (includeMiddle) {
    const [b, , ib] = miniUseState("age");
    trace.push({ hook: "useState(age)", slot: ib, value: b });
  }

  const [c, , ic] = miniUseState("effect-data");
  trace.push({ hook: "useEffect-ish", slot: ic, value: c });
  return trace;
}

export default function App() {
  const [renderNo, setRenderNo] = useState(0);
  const [includeMiddle, setIncludeMiddle] = useState(true);
  const trace = runMiniComponent(includeMiddle);
  rerender = () => setRenderNo((n) => n + 1);

  const misaligned = trace.some((t) => t.hook === "useEffect-ish" && t.value !== "effect-data");

  const th = { textAlign: "left", padding: "4px 14px 4px 0", borderBottom: "1px solid #ddd", fontSize: 13 };
  const td = { padding: "4px 14px 4px 0", fontFamily: "ui-monospace, monospace", fontSize: 13 };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <p>
        <label>
          <input
            type="checkbox"
            checked={includeMiddle}
            onChange={(e) => setIncludeMiddle(e.target.checked)}
          />{" "}
          call the middle hook
        </label>
      </p>

      <table style={{ borderCollapse: "collapse" }}>
        <thead>
          <tr><th style={th}>hook call</th><th style={th}>slot</th><th style={th}>value it read</th></tr>
        </thead>
        <tbody>
          {trace.map((t, i) => (
            <tr key={i}>
              <td style={td}>{t.hook}</td>
              <td style={td}>{t.slot}</td>
              <td style={{ ...td, color: t.hook === "useEffect-ish" && t.value !== "effect-data" ? "crimson" : "#161" }}>
                {String(t.value)}
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      <p style={{ marginTop: 12, color: misaligned ? "crimson" : "#161" }}>
        {misaligned
          ? "Misaligned: the third hook slid onto slot 1 and is now reading the age state."
          : "Aligned: every hook is reading its own slot."}
      </p>

      <p style={{ color: "#666", fontSize: 13 }}>
        Untick the box. Real React would throw "Rendered fewer hooks than
        expected" here β€” this miniature has no such check, so it shows you the
        corruption the check exists to prevent.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 93 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

Why does updating Context re-render all its consumers, and how do you fix it?

Continue