Skip to content

Rules & Custom Hooks

There are only two Rules of Hooks:

  1. Only call hooks at the top level — never inside conditions, loops, or nested functions.
  2. Only call hooks from React functions — components or other hooks, not plain functions.
function Profile({ userId }) {
const [user, setUser] = useState(null); // ✅ top level
if (!userId) {
const [error, setError] = useState(null); // ❌ conditional — breaks the rules
}
for (const id of ids) {
useEffect(() => { /* ... */ }); // ❌ in a loop — breaks the rules
}
}

Remember from the module intro: React matches each hook call to a memory slot by call order, not by name. The first useState is slot 1, the second is slot 2, every render.

flowchart TB
  r1["Render 1: useState, useState, useEffect
slots 1, 2, 3"] --> ok["consistent"]
  r2["Render 2: (condition false)
useState, useEffect
slots 1, 2"] --> bad["useEffect now reads slot 2
— the state of the wrong hook"]
A conditional hook shifts every later slot

If you call a hook conditionally, then on a render where the condition differs, the count and order of hook calls changes — and every hook after the conditional one reads the wrong slot. State bleeds between hooks; chaos follows. Calling hooks unconditionally, in the same order every render, is what keeps the slot mapping stable. That’s the entire reason for the rule.

Enforce it automatically with the official lint rules (eslint-plugin-react-hooks) — they catch conditional hooks and missing effect dependencies before they ship.

A custom hook is simply a function whose name starts with use and which calls other hooks. That’s it — there’s no special API. It lets you extract and reuse stateful logic (not UI) across components.

// A custom hook: reusable logic, its own state, calls built-in hooks.
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const on = () => setIsOnline(true);
const off = () => setIsOnline(false);
window.addEventListener('online', on);
window.addEventListener('offline', off);
return () => {
window.removeEventListener('online', on);
window.removeEventListener('offline', off);
};
}, []);
return isOnline;
}
// Any component can now use it:
function StatusBar() {
const isOnline = useOnlineStatus();
return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}

The crucial thing to understand: custom hooks share logic, not state. Each component that calls useOnlineStatus() gets its own independent state — the hook is a recipe that’s run fresh per component, not a shared store. (For shared state across components, you lift state up or use context/an external store — the next module.)

When to extract a custom hook: when two components need the same stateful behavior, or when a component’s logic gets complex enough that naming it (useForm, useDebouncedValue, useFetch) makes the component clearer.

What are the two Rules of Hooks?
Why can you not call a hook conditionally?
What is a custom hook?
Two components both call `useCounter()`. What do they share?