10 React Hooks Interview Questions That Actually Trip People Up (With Explanations)

javascript dev.to

If you've been prepping for React interviews, you've probably noticed most interview question lists online are either too basic (what is useState) or copy-pasted from the same five articles. So I put together a list of hooks questions that come up often in real interviews — the kind that sound simple but expose whether you actually understand what's happening under the hood.

Let's go through them.

Why does this useEffect cause an infinite loop?

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1);
  });

  return <div>{count}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Answer: No dependency array means the effect runs after every render.
Since it updates state on every run, it triggers another render, which
triggers the effect again — forever. Add [] if you want it to run once, or a proper dependency array if it should run on specific value changes.

What's the difference between useMemo and useCallback?

Both cache something between renders, but:

**useMemo **caches a value (the result of a computation)
**useCallback **caches a function reference

A common gotcha: useCallback(fn, deps) is literally equivalent to
useMemo(() => fn, deps). If you understand that, you understand both.

Why does my state update not reflect immediately after setState?

const [name, setName] = useState("Aman");

function handleClick() {
  setName("New Name");
  console.log(name); // still logs "Aman"
}
Enter fullscreen mode Exit fullscreen mode

Answer: State updates are **asynchronous **and batched. name inside
handleClick is a snapshot from the current render's closure — it won't
reflect the update until the component re-renders.

What problem does **useRef **solve that **useState **can't?

useRef gives you a mutable object that persists across renders without
triggering a re-render when it changes. Great for:

Storing a DOM node reference
Keeping a mutable value (like a timer ID) that shouldn't cause a re-render

Why do custom hooks need to start with "use"?

It's not just convention — it's how React's linter (and React itself) knows to apply the Rules of Hooks (no conditional calls, no calls inside loops) to your custom hook. Name it getSomething instead of useSomething and ESLint's hooks plugin won't catch violations inside it.

What's a stale closure, and how does it happen with hooks?

useEffect(() => {
  const interval = setInterval(() => {
    console.log(count); // always logs the initial value
  }, 1000);
  return () => clearInterval(interval);
}, []);
Enter fullscreen mode Exit fullscreen mode

Answer: The effect's closure captures count from the render it was
created in. With an empty dependency array, that closure never updates —
so count is frozen at its initial value forever inside the interval.

When would you use useLayoutEffect **instead of **useEffect?

Answer: useLayoutEffect runs synchronously before the browser paints. Use it when you need to measure or mutate the DOM before the user sees anything (e.g., preventing a visual flicker when adjusting an element's position). useEffect runs after paint, which is fine for most side effects (data fetching, subscriptions).

Can you conditionally call a hook?

No — and interviewers love asking why, not just that. React tracks hooks
by call order per render, not by name. If a hook is skipped in one render
and called in another, the entire hook-state mapping shifts, causing bugs
that are hard to trace.

What does the dependency array in useEffect actually compare?

Shallow equality (Object.is), not deep equality. This is why passing an
object or array literal as a dependency causes the effect to run on every
render — a new reference is created every time, even if the contents are
identical.

How would you **debounce **a search input using hooks?

function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}
Enter fullscreen mode Exit fullscreen mode

The cleanup function is the key part — it cancels the previous timer
whenever value changes before the delay finishes, so only the last
keystroke actually triggers the debounced update.

If you want to test yourself on questions like these interactively (with
instant feedback instead of just reading answers), I've been building
ReactGrind — a LeetCode-style platform specifically
for React.js practice. Still actively adding new challenges, so feedback is very welcome if you try it out.

What's a hooks question that stumped you in an interview? Drop it in the
comments — might turn it into a follow-up post.

Source: dev.to

arrow_back Back to Tutorials