ReactEasy

Rules of React hooks

01

The Short Answer

React hooks have two fundamental rules: only call hooks at the top level of your component (never inside conditions, loops, or nested functions), and only call hooks from React function components or custom hooks (never from regular JavaScript functions). These rules exist because React relies on the order of hook calls to correctly associate state with each hook between re-renders.

02

Rule 1: Only Call Hooks at the Top Level

Hooks must be called in the same order every time your component renders. This means you cannot put them inside if statements, loops, early returns, or nested functions. React uses the call order as an internal index to match each hook with its stored state — if the order changes between renders, React assigns the wrong state to the wrong hook.

top-level-rule.tsxtypescript
function UserProfile({ userId }: { userId: string }) {
  // ✅ Always called, always in the same order
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  // ❌ BROKEN — hook inside a condition
  if (userId) {
    useEffect(() => {
      fetchUser(userId).then(setName);
    }, [userId]);
  }

  // ✅ CORRECT — condition inside the hook
  useEffect(() => {
    if (userId) {
      fetchUser(userId).then(setName);
    }
  }, [userId]);

  return <div>{name} ({email})</div>;
}

The fix is always the same: call the hook unconditionally, and put the conditional logic inside the hook's callback. The hook itself must always execute — what it does internally can vary.

03

Why Order Matters

React doesn't use names to identify hooks — it uses their position in the call sequence. On every render, React walks through hooks in order: "first hook call = first piece of state, second hook call = second piece of state." If you skip a hook conditionally, every hook after it gets the wrong state.

The example below shows what goes wrong when a hook is conditionally skipped. On the first render all three hooks run, but on the second render the condition is false and the middle hook is skipped — now React's internal index is off by one and theme gets the value that belongs to language.

order-matters.tsxtypescript
function Settings({ showTheme }: { showTheme: boolean }) {
  const [name, setName] = useState('Alice');     // Hook #1 → state slot 0

  // ❌ If showTheme becomes false, this hook is skipped
  if (showTheme) {
    const [theme, setTheme] = useState('dark');   // Hook #2 → state slot 1
  }

  const [language, setLanguage] = useState('en'); // Hook #3 → state slot 2

  // Render 1 (showTheme=true):  hooks called = 3 → slots [0, 1, 2] ✅
  // Render 2 (showTheme=false): hooks called = 2 → slots [0, 1]
  //   React thinks hook #2 is 'language' but gives it slot 1 (theme's data) 💥
}

React's internal model

React stores hook state as a linked list attached to the component's fiber node. On each render, it walks the list in order. Position 0 = first useState, position 1 = second useState, etc. If the list length or order changes, the mapping breaks.

04

Rule 2: Only Call Hooks from React Functions

Hooks can only be called from two places: React function components and custom hooks (functions whose name starts with use). You cannot call hooks from regular JavaScript functions, class components, or event handlers. This ensures React can track which component owns each hook.

valid-locations.tsxtypescript
// ✅ Inside a function component
function SearchBar() {
  const [query, setQuery] = useState('');
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

// ✅ Inside a custom hook
function useDebounce(value: string, delay: number) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// ❌ Inside a regular function — React can't track this
function formatData(data: any[]) {
  const [sorted, setSorted] = useState(data); // ERROR
  return sorted;
}

// ❌ Inside an event handler
function Form() {
  function handleSubmit() {
    const [loading, setLoading] = useState(false); // ERROR
  }
}
05

Common Violations and Fixes

🔀

Hook after early return

Placing a hook after a conditional `return` statement means it won't execute on some renders. This breaks the call order rule.

Move all hooks above any early returns. The hook must always run — put the conditional logic inside the hook or after all hooks are declared.

🔁

Hook inside a loop

Calling `useState` or `useEffect` inside a `.map()` or `for` loop means the number of hook calls varies with the array length — React can't handle this.

Extract the loop body into a separate component. Each instance of that component has its own stable hook calls.

🎣

Hook inside a nested function or callback

Calling a hook inside `setTimeout`, `addEventListener`, or a Promise `.then()` callback means it runs outside React's render cycle.

Hooks must be called synchronously during the component's render. Move the hook to the top level and use its setter inside the callback instead.

06

The ESLint Plugin

React provides eslint-plugin-react-hooks with two rules that catch violations at development time. The rules-of-hooks rule enforces both rules (top-level only, React functions only). The exhaustive-deps rule checks that your useEffect and useCallback dependency arrays include all values used inside the callback.

eslint-config.jsonjson
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Never disable rules-of-hooks — violations cause silent, hard-to-debug state corruption. The exhaustive-deps rule can occasionally be wrong for advanced patterns, but treat warnings as bugs until proven otherwise.

07

Why Interviewers Ask This

This question checks whether you understand the constraints of React's hook system and why they exist. Interviewers want to hear that you know hooks rely on call order (not names), can explain what breaks when rules are violated, and know the practical patterns for working within these constraints (conditions inside hooks, extracting components for loops). It shows you understand React's internals well enough to avoid subtle bugs.

Quick Revision Cheat Sheet

Top level only: No hooks inside if/else, loops, nested functions, or after early returns

React functions only: Only in function components and custom hooks (use* prefix)

Why order matters: React uses call position as an index into its state storage

Conditional logic: Put conditions INSIDE the hook, not around it

Loops: Extract loop body into a child component with its own hooks

Enforcement: eslint-plugin-react-hooks catches violations at dev time