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.

RuleWhat it meansWhy
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

Signatureconst [state, setState] = useState<T>(initialValue)
Lazy inituseState(() => expensiveCompute()), function called only once.
Functional updatesetState(prev => prev + 1), safe when new state depends on old.
Object stateUpdate with spread: setState(prev => ({ ...prev, key: value })).
ResetsetState(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

PatternCodeMeaning
Run on every renderuseEffect(() => { ... })No deps array; usually wrong.
Run once on mountuseEffect(() => { ... }, [])Empty deps; replaces componentDidMount.
Run when deps changeuseEffect(() => { ... }, [a, b])Re-runs when a or b changes (referential equality).
Cleanupreturn () => { ... } inside the effectRuns before the effect re-runs and on unmount.
Async insideWrap 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.

HookReturnsUse 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 casePatternNotes
DOM node referenceconst ref = useRef<HTMLInputElement>(null); <input ref={ref} />ref.current is the DOM element after mount.
Mutable instance variableconst countRef = useRef(0); countRef.current++Does not trigger re-render when changed.
Persist values across rendersconst prevValue = useRef(value)Store previous render’s value without state.
Stable callback refconst 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

HookSignatureWhen 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

  • useEffect with an object or array dep fires on every render because referential equality fails. Memoize the dep with useMemo or 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.
  • useRef changes do not trigger re-renders. Reading ref.current in JSX will show stale values. Use state for anything that must re-render.
  • useContext re-renders every consumer when the context value changes, even if the consumer only reads one field. Split contexts or memoize the value with useMemo.
  • useMemo and useCallback are 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 setState inside useEffect without correct deps creates infinite loops. Always ask: does this effect write to a dep it also reads?