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.
Understand
Keep ordinary Hook calls outside conditions, loops and handlers.
Build
Place early returns after the component’s Hooks.
Check
Extract a component when a conditional part needs its own state.
Stable calls, conditional markup
Call Hooks
The component declares its state before branching.
Choose markup
A condition decides which interface to describe.
Render again
Hook order stays the same as values change.
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.
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.