Follow the Rules of Hooks
Call Hooks only at the top level of a function component or custom Hook, in the same order on every render, and before any early return.
Implementation
- Do not call Hooks inside conditions, loops, nested functions, event handlers, class components, or
try/catchblocks. - Move a condition inside the Hook call, or into the logic that uses the Hook's result, instead of wrapping the Hook call in the condition.
- When a component needs a varying number of stateful items, render a child component per item, and let each child call its own Hooks.
- The
useAPI is an exception: it can be called conditionally or in a loop, but only while rendering a component or Hook, and not insidetry/catch. - Enable the React Hooks lint rules. When they report a call-order or dependency problem, change the structure instead of suppressing the warning, unless a comment documents why the suppression is safe.
Rationale
React identifies each Hook's state by the order of Hook calls during render. If a call is skipped or repeated on one render, every later Hook reads the state that belonged to a different call. The resulting bugs appear far from the conditional call, often only for some props.
Examples
Application: A conditional Hook
Incorrect (counterexample):
function Profile({ enabled }: { enabled: boolean }) {
if (enabled) {
const [name, setName] = useState('');
}
return null;
}
The Hook runs on some renders and not others, so React loses track of which state belongs to which call.
Correct:
function Profile({ enabled }: { enabled: boolean }) {
const [name, setName] = useState('');
if (!enabled) {
return null;
}
return <input value={name} onChange={(event) => setName(event.target.value)} />;
}
Application: A Hook per list item
Incorrect (counterexample):
function Checklist({ items }: { items: ReadonlyArray<Item> }) {
const checked = items.map(() => useState(false));
// ...
}
The number of Hook calls changes whenever the list length changes.
Correct:
function Checklist({ items }: { items: ReadonlyArray<Item> }) {
return items.map((item) => <ChecklistRow key={item.id} item={item} />);
}
function ChecklistRow({ item }: { item: Item }) {
const [checked, setChecked] = useState(false);
// ...
}
Each row owns a fixed set of Hook calls.
Validation
Run the React Hooks lint rules and check that they report no call-order violations. Review any lint suppression for a comment that explains why it is safe.
A conditional or looped call to use is not a violation.