React hooks have been the dominant model for component logic since 2019. By 2026, every React-based interview probes hooks knowledge; senior+ interviews probe it deeply. The questions go beyond “what does useState do?” into the corners where hooks misbehave, the patterns that signal real fluency, and the pitfalls that catch even experienced developers.
This piece covers the hooks knowledge senior interviewers actually test, the common pitfalls, and how to prepare for the round.
The Rules of Hooks (and why)
Two rules:
- Only call hooks at the top level. Not inside loops, conditions, or nested functions.
- Only call hooks from React function components or custom hooks. Not from regular JavaScript functions.
Senior interviewers ask why these rules exist. The answer: React tracks hook state via call order, not by name. The first useState call in a component is associated with one piece of state; the second is another. If hooks were called conditionally, React’s tracking would lose alignment between renders, and state would shuffle unpredictably.
This explanation matters because it informs every hook design decision. Custom hooks that branch internally must do so within their own state — the outer call signature has to be stable.
The dependency array
Most hooks (useEffect, useMemo, useCallback) take a dependency array. Three rules:
- List every value from component scope used inside the hook. If you use a state, prop, or computed value, it must be in the array.
- Stable references stay stable. Refs, set-state functions, and dispatch functions are stable across renders and don’t need to be listed (though listing them is not wrong).
- Object and array literals create new references each render. If you put
{ foo: 1 }in a dependency array, it triggers the effect on every render because the object identity changes.
The most common interview test: show a useEffect with a missing dependency or with an unstable reference. Ask the candidate to identify the bug.
Common pitfall: stale closures
The classic bug:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // BUG: stale closure on count
}, 1000);
return () => clearInterval(id);
}, []); // BUG: missing count from deps
return <div>{count}</div>;
}
The interval captures count at mount time. The closure stays at 0 forever. The counter increments to 1 and stops.
Two fixes:
// Fix 1: functional updater (no need to read current state)
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// Fix 2: include count in deps and re-create the interval
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
The functional-updater fix is generally preferred. Senior candidates know both and explain when each applies.
Common pitfall: infinite loops
A useEffect with a state setter inside that sets state on every run causes an infinite loop. Common forms:
// Bad: object reference changes every render, triggers effect, sets state, repeat
useEffect(() => {
setData({ ...data, computed: true });
}, [data]);
// Bad: missing deps cause stale closures, but also you might want this:
useEffect(() => {
setOptions(getOptions(input));
}, [input]); // safer if getOptions is pure
Strong candidates spot the loop pattern from a quick read.
Common pitfall: not unmounting properly
Effects that subscribe to something (event listeners, timers, WebSocket connections) must clean up:
useEffect(() => {
const handler = () => console.log('click');
window.addEventListener('click', handler);
return () => window.removeEventListener('click', handler);
}, []);
Forgetting cleanup is a memory leak and also a bug-prone pattern in StrictMode (where effects run twice in development to surface the cleanup mistake).
Common pitfall: useEffect for derived state
Using useEffect to compute derived state from props is almost always wrong:
// Bad
function Filter({ items, query }) {
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter(i => i.includes(query)));
}, [items, query]);
return <ul>{filtered.map(...)}</ul>;
}
// Good
function Filter({ items, query }) {
const filtered = useMemo(
() => items.filter(i => i.includes(query)),
[items, query]
);
return <ul>{filtered.map(...)}</ul>;
}
// Best (no memo if computation is cheap)
function Filter({ items, query }) {
const filtered = items.filter(i => i.includes(query));
return <ul>{filtered.map(...)}</ul>;
}
Senior interviewers test for this pattern. The first version causes an extra render; the second computes during render; the third (when computation is cheap) is the simplest.
Custom hooks: when and how
Custom hooks extract reusable logic. Strong patterns:
- useDebouncedValue — wraps useState with a debounced setter.
- useLocalStorage — useState that persists to localStorage.
- useMediaQuery — boolean for window.matchMedia.
- useEventListener — adds an event listener with cleanup.
- useFetch / useQuery — data fetching with loading and error states.
Senior interviews ask candidates to design a custom hook for a specific use case. The candidate should articulate the API, identify state and effects, and handle edge cases (cancellation, race conditions in fetch hooks, etc.).
Common interview question: implement useDebouncedValue
function useDebouncedValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debouncedValue;
}
The interview signal: does the candidate clean up the timeout? Does the effect have correct dependencies? Does the candidate consider what happens if delay changes mid-cycle?
What scores well
- Articulating why the rules of hooks exist, not just what they say.
- Diagnosing stale closures from a quick code read.
- Knowing when to use functional updaters vs reading current state.
- Treating useEffect as a last resort, not a default.
- Understanding that custom hooks share logic, not state.
What scores poorly
- Memorized hook signatures without understanding behavior.
- Using useEffect for derived state that could be computed during render.
- Missing dependency-array entries.
- Not handling effect cleanup.
- Confusing useMemo and useCallback.
What changed with React Compiler
React Compiler (shipped in React 19+) auto-memoizes components and computes equivalent of useMemo/useCallback automatically. Effect on the interview:
- Manual memoization knowledge still tested — interviewers want to see you understand the underlying concepts even if the compiler handles them.
- Stale closure understanding still tested — the compiler does not solve this.
- Dependency array reasoning still tested — even with the compiler, custom hooks still need correct dependency tracking.
The compiler reduces the manual memoization burden but does not eliminate the need to understand hooks deeply.
Frequently Asked Questions
Are hooks still the dominant model in 2026?
Yes. Functional components with hooks are the default. Class components persist in legacy code but are not the model new code is written in.
Should I learn React Compiler internals?
Conceptual understanding yes; deep internals no. The compiler is implementation; the interview tests the abstractions.
How important is useReducer vs useState?
useReducer for complex state with multiple actions; useState for simple state. Senior candidates know when each fits but the distinction is not heavily tested.
What about Context performance?
Context updates re-render all consumers. Senior interviews test whether candidates know to split contexts when only some consumers need a particular value, or to use a state-management library (Zustand, Jotai) for fine-grained subscriptions.
Are React Query / SWR data hooks tested?
Conceptually yes — senior frontend interviews increasingly assume familiarity with at least one async-data library. The patterns (stale-while-revalidate, cache invalidation, optimistic updates) are tested.