Overview
A hooks-heavy file forces three decisions: which state primitive, whether to write an Effect at all, and how to name and shape a custom hook. The rules are mechanical and enforced by eslint-plugin-react-hooks. The umbrella rule set lives in react; memoization lives in react-compiler.
Call hooks at the top level, every render
Call hooks only at the top level of a component or another hook: never inside a condition, loop, event handler, class, or after an early return. React tracks hooks by call order, so a conditional hook desyncs the component. use() is the exception and may be called inside conditions and loops.
// Wrong: hook after a conditional return.
if (!user) return null;
const [count, setCount] = useState(0);
// Right: hooks first, branch afterward.
const [count, setCount] = useState(0);
if (!user) return null;Run eslint-plugin-react-hooks in CI. Treat rules-of-hooks as an error. exhaustive-deps catches stale closures; silence it only with a comment that says why.
Pick useState for atoms, useReducer for coordinated shape
Use useState for a single independent value. Use useReducer when several values change together, when the next state depends on the previous one in non-trivial ways, or when a test should assert on a pure transition function. Three useState calls that always update together are one reducer.
const [open, setOpen] = useState(false);
const [state, dispatch] = useReducer(reducer, { status: "idle", items: [], error: null });Treat useEffect as the escape hatch
Write an Effect only to synchronize with something outside React: a DOM API, a subscription, a timer, a network connection, an analytics call. Compute derived values during render, read URL state from the router, and mutate through event handlers and server actions.
// Wrong: Effect mirrors props into state, costing an extra render.
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// Right: derive during render.
const full = `${first} ${last}`;Always return cleanup for subscriptions, timers, and listeners. When an Effect must read the latest props or state without re-running when they change, extract that part with useEffectEvent (React 19.2). An Effect Event is called from inside the Effect and never appears in the dependency array.
const onConnected = useEffectEvent(() => showToast(`Connected (${theme})`));
useEffect(() => {
const conn = connect(roomId);
conn.on("connected", () => onConnected());
return () => conn.disconnect();
}, [roomId]); // theme changes do not reconnectName custom hooks useThing and return one shape
A custom hook is any function that calls another hook. The use prefix lets the linter enforce the rules at call sites. Return a tuple for two or three values and an object beyond that. Use a hook when components share stateful or Effect logic; use a plain function when the logic is pure.
function useDebouncedValue<T>(value: T, ms = 200): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(id);
}, [value, ms]);
return debounced;
}Memoize only when measured
useMemo and useCallback on cheap expressions add bookkeeping and slow the render. With the compiler off, add them only when the Profiler shows a cost. With it on, they remain for Effect-dependency stability and a few other cases. The full rule is in react-compiler.
Read context, do not store state in it
useContext (or use(Context)) reads a value; it does not manage one. Pair it with useReducer or a store when the value changes, and keep high-frequency values (pointer position, scroll offset, form fields) out of context because every consumer re-renders. See react-state-management.