Back to Notes

React Hooks Deep-Dive

useState

const [count, setCount] = useState(0);
setCount(prev => prev + 1);   // functional form — required when the new state depends on the old

State updates are asynchronous/batched — reading count immediately after calling setCount still shows the old value in the current render. The functional-updater form (prev => ...) is what protects against stale-closure bugs when multiple updates are queued before a re-render.

useEffect

useEffect(() => {
  const subscription = subscribe();
  return () => subscription.unsubscribe();   // cleanup — runs before the next effect, and on unmount
}, [dependency]);

[] = mount only; [dep] = re-run when dep changes; no array at all = every render (rarely what's actually wanted). The most common real bug: a missing dependency causes a stale closure — the effect captures the value of a variable as it was when the effect was created, and won't see later updates to it unless that variable is listed in the dependency array.

useRef

const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();          // direct DOM access

const timerRef = useRef(null);
timerRef.current = setTimeout(...); // mutable value that does NOT trigger a re-render when changed

The core distinction from useState: mutating .current never causes a re-render — useRef is for values a component needs to remember across renders without that value being part of what's displayed (a timer ID, a previous prop value, a DOM node reference).

useMemo / useCallback

const sorted = useMemo(() => items.sort(cmp), [items]);      // memoize a computed VALUE
const handleClick = useCallback(() => doSomething(id), [id]); // memoize a FUNCTION reference

useCallback(fn, deps) is functionally useMemo(() => fn, deps) — both exist to prevent unnecessary work: useMemo skips recomputation, useCallback gives a stable function reference so a memoized child component (React.memo) doesn't see "a new prop" on every parent render just because a new function object was created. Use sparingly — premature memoization adds real code complexity for often-negligible gain; reach for it after profiling shows an actual re-render cost, not reflexively on every function/computation.

useContext

const ThemeContext = React.createContext('light');
<ThemeContext.Provider value="dark"><Child /></ThemeContext.Provider>
const theme = useContext(ThemeContext);

Solves prop drilling (passing a value through many layers of components that don't themselves need it) — but every consumer re-renders whenever the Provider's value changes, which is why Context isn't a full substitute for a proper state management library at high-frequency-update scale.

useReducer

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset': return { count: 0 };
    default: throw new Error();
  }
}
const [state, dispatch] = useReducer(reducer, { count: 0 });

Preferred over multiple useState calls when state has several sub-values that update together, or when the next state genuinely depends on the action type in a way a switch statement expresses more clearly than several independent setters.

Custom Hooks — Extracting Stateful Logic

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    fetch(url).then(r => r.json()).then(d => { setData(d); setLoading(false); });
  }, [url]);
  return { data, loading };
}

A custom hook is just a function that calls other hooks — it doesn't create a separate "instance" of state per call site the way a class would; each component calling useFetch gets its own independent state, because hooks are tied to the calling component's render, not to the hook function itself.

Interview Drills

  1. Why does setCount(count + 1) called twice in the same event handler only increment once, while setCount(prev => prev + 1) called twice increments twice? — React batches state updates within an event handler; setCount(count + 1) both times reads the same stale count from the render closure, so both calls compute the same new value — the functional updater form always receives the truly latest pending state, letting each call build on the previous one correctly.
  2. A useEffect reads a prop but doesn't list it in the dependency array. What bug results? — A stale closure — the effect's callback closes over the prop's value as it was on the render the effect was created in, and won't see subsequent updates to that prop unless it's included in the dependency array, causing the effect to act on outdated data.
  3. When would useReducer be a clearer choice than several separate useState calls? — When multiple pieces of state update together as a unit in response to the same action (so tracking "what changed together" is easier to read as named action types than as several separate setter calls scattered through event handlers), or when the next state's shape genuinely depends on the current state plus the action, which a reducer function expresses more directly.

Cross-links

[[JS Closures & Async]] · [[React Patterns & State Management]] · [[React Performance & Reconciliation]]