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
- Why does
setCount(count + 1)called twice in the same event handler only increment once, whilesetCount(prev => prev + 1)called twice increments twice? — React batches state updates within an event handler;setCount(count + 1)both times reads the same stalecountfrom 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. - A
useEffectreads 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. - When would
useReducerbe a clearer choice than several separateuseStatecalls? — 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]]