ProgramGuru

Rules of Hooks

Rules of Hooks

An attendance component sometimes calls useState only after the teacher opens a panel. After the panel changes visibility, React cannot rely on the same sequence of stateful calls.

Hooks are React functions that connect a component to features such as state and Effects. Ordinary Hooks such as useState, useRef and useEffect must be called at the top level of a function component or custom Hook, in a stable order across renders.

  1. Understand

    Keep ordinary Hook calls outside conditions, loops and handlers.

  2. Build

    Place early returns after the component’s Hooks.

  3. Check

    Extract a component when a conditional part needs its own state.

Stable calls, conditional markup

  1. Call Hooks

    The component declares its state before branching.

  2. Choose markup

    A condition decides which interface to describe.

  3. Render again

    Hook order stays the same as values change.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

Keep the sequence predictable

React associates an ordinary Hook call with its place in the component’s call sequence. If a conditional branch adds or removes a useState call, later calls no longer follow the same sequence. The rule is about where the Hook executes, not whether the variable name stays the same.

Top level means in the body of a component or custom Hook, outside loops and nested functions, and before a conditional early return. Calling a setter in an event handler is allowed; calling useState itself inside that handler is not. The handler uses a Hook result that the component already obtained during rendering.

Toggle a panel without changing Hook order

App calls both useState Hooks every time it renders. The show condition chooses which JSX to return, so the count can remain remembered while the attendance panel is hidden. This is a useful distinction: conditionally displaying content does not require conditionally calling Hooks.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [show, setShow] = useState(true);
  const [present, setPresent] = useState(0);
  return <main><h1>Stable Hook order</h1>
    <button onClick={() => setShow(value => !value)}>{show ? 'Hide panel' : 'Show panel'}</button>
    {show ? <section><p>Present: {present}</p>
      <button onClick={() => setPresent(value => value + 1)}>Mark present</button>
    </section> : <p>Attendance panel hidden</p>}
  </main>;
}

Expected screen and behavior

Mark present increases the count. Hide panel removes its markup. Show panel brings it back with the same App-owned count.

The condition changes output, not declarations

React · Toggle a panel without changing Hook order

import { useState } from 'react';
export default function App() {
  const [show, setShow] = useState(true);
  const [present, setPresent] = useState(0);
  return <main><h1>Stable Hook order</h1>
    <button onClick={() => setShow(value => !value)}>{show ? 'Hide panel' : 'Show panel'}</button>
    {show ? <section><p>Present: {present}</p>
      <button onClick={() => setPresent(value => value + 1)}>Mark present</button>
    </section> : <p>Attendance panel hidden</p>}
  </main>;
}

Initially show is true and present is zero. Clicking Mark present invokes a setter from the earlier Hook call. App renders with the updated count, but still calls show state first and present state second. The conditional section now displays one.

Hiding the panel changes show to false. App still calls both Hooks before choosing its returned markup. The present state remains part of App’s identity even though its paragraph is temporarily absent. If the count belonged to a separately mounted child that was removed, its local state would reset when that child was mounted again.

Extract stateful conditional parts

When a conditional part is a separate task with its own state, extract a component and conditionally render that component. That child can call its Hooks at its own top level. Removing the child deliberately ends its identity and local state; preserving an App-owned count is a separate ownership decision.

Custom Hooks also follow these rules and conventionally start with use. A normal utility that only calculates a value does not become a Hook by receiving state as an argument. React’s newer use API has specifically documented conditional-call exceptions; those exceptions do not apply to useState or useEffect in this lesson.

Try the school task

React · starting example

import { useState } from 'react';
export default function App() {
  const [show, setShow] = useState(true);
  const [present, setPresent] = useState(0);
  return <main><h1>Stable Hook order</h1>
    <button onClick={() => setShow(value => !value)}>{show ? 'Hide panel' : 'Show panel'}</button>
    {show ? <section><p>Present: {present}</p>
      <button onClick={() => setPresent(value => value + 1)}>Mark present</button>
    </section> : <p>Attendance panel hidden</p>}
  </main>;
}

Build App with a locked flag and a present count. When locked, return a message and an Unlock button. When unlocked, show Mark present. Keep both Hooks before the early return so locking and unlocking do not change their order. The count must survive a lock cycle.

Worked answer

The answer declares both state values before checking locked. The conditional return changes the available actions, while the Hook declarations remain stable. Mark once, lock, and unlock: Present: 1 returns because the state owner has stayed mounted throughout.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [locked, setLocked] = useState(true);
  const [present, setPresent] = useState(0);
  if (locked) return <main><h1>Register locked</h1>
    <button onClick={() => setLocked(false)}>Unlock</button></main>;
  return <main><h1>Register open</h1><p>Present: {present}</p>
    <button onClick={() => setPresent(value => value + 1)}>Mark present</button>
    <button onClick={() => setLocked(true)}>Lock register</button>
  </main>;
}

Expected screen and behavior

Unlock shows Present: 0. Mark once, lock and unlock: the count remains 1.

Mistakes and boundaries

Do not call Hooks inside map to give each row state. Create a StudentRow component whose own top level declares its state, then render rows with stable keys. Calling a component function directly to bypass that structure is also not the normal React component model.

Enable the official React Hooks lint rules in real project tooling. A lint check can identify many structural mistakes and missing Effect dependencies, but it does not prove the school workflow is correct. You still need to check which values survive hiding, replacement and student changes.

Check your reasoning

React · Toggle a panel without changing Hook order

import { useState } from 'react';
export default function App() {
  const [show, setShow] = useState(true);
  const [present, setPresent] = useState(0);
  return <main><h1>Stable Hook order</h1>
    <button onClick={() => setShow(value => !value)}>{show ? 'Hide panel' : 'Show panel'}</button>
    {show ? <section><p>Present: {present}</p>
      <button onClick={() => setPresent(value => value + 1)}>Mark present</button>
    </section> : <p>Attendance panel hidden</p>}
  </main>;
}

Choose the answer that follows the data and the interaction, then read the explanation for each alternative.

Which pattern keeps useState calls valid while showing a panel conditionally?
Choose one answer. You can try again.

Answer and explanation

Call useState at the component top level and conditionally return panel markup.

Correct. The Hook sequence stays stable while the returned interface changes.

Carry the idea forward

Declare ordinary Hooks in stable top-level positions and let conditions choose markup or component identities. Next learn useRef for remembered values that should not themselves request a rendered update.

Read the primary reference: React: rules of Hooks.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Remembering Values with useRef →