Overview
Hooks let function components manage state, side-effects, and shared logic without class components. This card is the lookup table; the performance trade-offs between useMemo, useCallback, and memo are discussed in react-performance. All examples use TypeScript generics where types are non-obvious.
Rules of hooks (non-negotiable)
Violating these causes silent bugs or runtime crashes.
| Rule | What it means | Why |
|---|---|---|
| Call only at the top level. | Never inside conditions, loops, or nested functions. | React tracks hooks by call order. Branching breaks the order. |
| Call only from React functions. | Function components and custom hooks only, not plain JS functions. | React must own the component lifecycle to manage state. |
Prefix custom hooks with use. | useMyHook, not myHook. | Enables lint rules and signals hook semantics to readers. |
The react-hooks/rules-of-hooks ESLint rule enforces these automatically.
useState
| Signature | const [state, setState] = useState<T>(initialValue) |
|---|---|
| Lazy init | useState(() => expensiveCompute()), function called only once. |
| Functional update | setState(prev => prev + 1), safe when new state depends on old. |
| Object state | Update with spread: setState(prev => ({ ...prev, key: value })). |
| Reset | setState(initialValue), no special API; just set it back. |
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Functional update avoids stale closure
setCount(prev => prev + 1);
// Partial object update
setUser(prev => prev ? { ...prev, name: "Alice" } : null);useEffect
| Pattern | Code | Meaning |
|---|---|---|
| Run on every render | useEffect(() => { ... }) | No deps array; usually wrong. |
| Run once on mount | useEffect(() => { ... }, []) | Empty deps; replaces componentDidMount. |
| Run when deps change | useEffect(() => { ... }, [a, b]) | Re-runs when a or b changes (referential equality). |
| Cleanup | return () => { ... } inside the effect | Runs before the effect re-runs and on unmount. |
| Async inside | Wrap in an immediately-invoked async function; don’t make the effect itself async. | useEffect must return either nothing or a cleanup function. |
useEffect(() => {
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(() => {}); // swallow abort errors
return () => controller.abort();
}, []);useMemo and useCallback
Both accept a deps array. Omit only when the computation is trivial.
| Hook | Returns | Use when |
|---|---|---|
useMemo(() => compute(), deps) | Memoized value. | The computation is expensive (sort, filter, map on large arrays). |
useCallback(fn, deps) | Memoized function reference. | Passing a callback to a memoized child that uses React.memo. |
// useMemo: sorted list only recomputes when items changes
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// useCallback: stable reference for memoized child
const handleClick = useCallback((id: number) => {
dispatch({ type: "select", id });
}, [dispatch]);Do not wrap every value in useMemo. The memoization overhead costs more than a cheap recalculation.
useRef
| Use case | Pattern | Notes |
|---|---|---|
| DOM node reference | const ref = useRef<HTMLInputElement>(null); <input ref={ref} /> | ref.current is the DOM element after mount. |
| Mutable instance variable | const countRef = useRef(0); countRef.current++ | Does not trigger re-render when changed. |
| Persist values across renders | const prevValue = useRef(value) | Store previous render’s value without state. |
| Stable callback ref | const cbRef = useRef(cb); useEffect(() => { cbRef.current = cb }, [cb]) | Lets an interval/listener always call the latest version. |
useRef is the right choice when you need a mutable value that does not affect render output.
useContext and useReducer
| Hook | Signature | When to use |
|---|---|---|
useContext(Ctx) | Returns current context value. | Read theme, auth, locale; avoid for high-frequency updates. |
useReducer(reducer, init) | [state, dispatch] pair. | State with multiple sub-values or complex transitions; replaces multiple useState calls. |
// useReducer for a form with multiple fields
type Action =
| { type: "set_name"; value: string }
| { type: "set_email"; value: string }
| { type: "reset" };
function formReducer(state: FormState, action: Action): FormState {
switch (action.type) {
case "set_name": return { ...state, name: action.value };
case "set_email": return { ...state, email: action.value };
case "reset": return initialState;
}
}
const [form, dispatch] = useReducer(formReducer, initialState);Common gotchas
useEffectwith an object or array dep fires on every render because referential equality fails. Memoize the dep withuseMemoor move it outside the component.useCallback(fn, [])with a stale closure captures the values at mount time. Add all values the callback reads to the deps array.useRefchanges do not trigger re-renders. Readingref.currentin JSX will show stale values. Use state for anything that must re-render.useContextre-renders every consumer when the context value changes, even if the consumer only reads one field. Split contexts or memoize the value withuseMemo.useMemoanduseCallbackare hints, not guarantees. React may discard memoized values between renders (e.g., in React 18 Concurrent Mode). Do not store side-effectful logic inside them.- Calling
setStateinsideuseEffectwithout correct deps creates infinite loops. Always ask: does this effect write to a dep it also reads?