Rules of Hooks explained: why they exist, what breaks when violated, and how to structure custom hooks correctly.
Rules of Hooks explained: why they exist, what breaks when violated, and how to structure custom hooks correctly.
BeforeMerge offers hundreds of code review rules, guides, and detection patterns to help your team ship better code.
Hooks rely on a stable call order between renders. Violating the rules causes bugs that are difficult to diagnose.
Do not call hooks inside loops, conditions, or nested functions.
// BAD — conditional hook
function Profile({ userId }: { userId?: string }) {
if (userId) {
const [user, setUser] = useState(null); // breaks!
}
}
// GOOD — unconditional hook, conditional logic inside
function Profile({ userId }: { userId?: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (userId) fetchUser(userId).then(setUser);
}, [userId]);
}Why: React tracks hooks by their call index. If a hook is conditionally skipped, all subsequent hooks shift positions, causing React to return the wrong state for each hook.
Call hooks only from:
use)// BAD — hook in a regular function
function getUser() {
const [user] = useState(null); // not a component or hook!
return user;
}
// GOOD — custom hook
function useUser() {
const [user, setUser] = useState(null);
useEffect(() => { fetchUser().then(setUser); }, []);
return user;
}| Violation | Symptom |
|---|---|
Hook inside if |
State values swap between hooks unpredictably |
| Hook inside loop | Crash on re-render when array length changes |
| Hook in regular function | React error: hooks called outside component |
| Hook after early return | Fewer hooks than expected error |
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}Best practices:
useuseEffect returnInstall the official plugin:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}This catches most violations at lint time rather than runtime.