React hooks transformed how React components work after their introduction in React 16.8 (2019). Most developers use useState and useEffect comfortably but have a shallower understanding of the remaining hooks. Here is what each actually does and when to reach for it.
useState and useReducer: The State Hooks
`useState`: manages simple state values. The key mental model: calling `setState` schedules a re-render; the component re-renders, and the new state value is the argument you passed. The stale closure problem: if you access state inside a closure (setTimeout, event listener added without proper cleanup, async function), you may see the state value from when the closure was created, not the current value. Fix: use the functional update form `setState(prev => prev + 1)` which always receives the current value. `useReducer`: manages more complex state transitions where the next state depends on both the action and the current state. The pattern: `const [state, dispatch] = useReducer(reducer, initialState)` where reducer is `(state, action) => newState`. When to use useReducer over useState: when you have multiple related state values that change together; when the next state depends on the current state in complex ways; when state update logic is complex enough that moving it outside the component (into a pure reducer function) makes it more testable. The performance difference between useState and useReducer is negligible — choose based on code clarity, not performance.
useEffect and its Siblings
`useEffect(fn, deps)`: runs `fn` after every render where at least one value in `deps` changed. The mental model shift: useEffect is not a lifecycle method — it is a synchronisation mechanism. The question is not “when should this run?” but “with what state/props should this side effect be in sync?” The cleanup function: the function returned from useEffect runs before the next effect execution and on unmount — use it to cancel network requests, clear timeouts, remove event listeners, unsubscribe from subscriptions. The empty dependency array `[]`: runs once after the first render and again on unmount cleanup. This is the “on mount” equivalent but be careful — it is easy to capture stale values if the effect’s internal code references variables not listed as dependencies. `useLayoutEffect`: runs synchronously after DOM mutations but before the browser paints. Use it only when you need to measure DOM elements or make DOM mutations that must happen before paint (to avoid visual flickering). For 95% of use cases, `useEffect` is correct. `useInsertionEffect`: for CSS-in-JS libraries only — runs before DOM mutations. Never use this unless you are writing a styling library.
useCallback, useMemo, and useRef
`useCallback(fn, deps)`: memoizes a function — returns the same function reference between renders if deps haven’t changed. The use case: passing a callback to a child component that is wrapped in `React.memo`. Without useCallback, the child re-renders every time the parent renders because the function reference is new each render. The over-use problem: adding useCallback everywhere is a common over-optimisation — the memoisation itself has a cost. Only add it when you have evidence of unnecessary re-renders (React DevTools profiler). `useMemo(fn, deps)`: memoizes the return value of a function. Use when the computation is expensive and the result is only needed when specific dependencies change. The test: if the function takes less than 1ms, useMemo adds more overhead than it saves. `useRef(initialValue)`: creates a mutable ref object whose `.current` property persists across renders without causing re-renders. Two distinct uses: accessing DOM elements (`




