Skip to content
React interview 5 min read

Hooks Interview Questions

Hooks are where React interviews get sharp. Anyone can call useState, but interviewers probe whether you understand the rules that make hooks work, how effect dependencies and cleanup behave, and the stale-closure traps that bite even experienced developers. This set of 18 question-and-answer pairs covers the rules of hooks, useEffect, memoization, refs, custom hooks, and a few output-prediction puzzles, all in modern React 18/19 terms.

Rules of hooks

What are the two rules of hooks?

Only call hooks at the top level of a component or another hook, never inside loops, conditions, or nested functions. And only call hooks from React function components or custom hooks, never from regular JavaScript functions. Both rules ensure React can reliably match each hook call to its stored state across renders.

Why must hooks be called in the same order every render?

React identifies a hook by its call order, not by name. It keeps state in an internal list indexed by position. If you wrap a hook in a condition, the index shifts on some renders, and React associates the wrong state with the wrong hook, corrupting your component.

// Wrong: conditional hook breaks the call order
function Profile({ user }) {
  if (user) {
    const [name, setName] = useState(user.name); // never do this
  }
}

The ESLint plugin eslint-plugin-react-hooks enforces both rules automatically. Mentioning it in an interview signals real-world experience.

Can you call a hook inside an event handler?

No. Event handlers run after render and outside React’s hook-tracking phase. Call the hook at the top level and use the returned value or setter inside the handler instead.

useEffect and dependencies

When does useEffect run?

After the browser has painted the committed render. With a dependency array, it re-runs only when one of those values changes between renders. An empty array runs it once after mount; omitting the array runs it after every render.

What is the dependency array and what belongs in it?

It lists every reactive value the effect reads: props, state, and anything derived from them. React compares each entry with Object.is against the previous render. Omitting a used value causes stale data; the linter flags this as react-hooks/exhaustive-deps.

How does effect cleanup work?

If an effect returns a function, React runs it before the next effect execution and on unmount. Use it to undo subscriptions, timers, or listeners so they do not leak or fire after the component is gone.

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);
  return () => clearInterval(id);
}, []);

How do you cancel a fetch in an effect to avoid race conditions?

Track an ignore flag or use an AbortController in cleanup so a stale response cannot overwrite fresh state.

useEffect(() => {
  let ignore = false;
  fetch(`/api/users/${id}`)
    .then((r) => r.json())
    .then((data) => {
      if (!ignore) setUser(data);
    });
  return () => {
    ignore = true;
  };
}, [id]);

Stale closures

What is a stale closure in React?

Every render creates fresh functions that close over that render’s props and state. If an effect or callback runs later but captured an old render’s variables, it reads outdated values. This is the most common useEffect bug.

Predict the output of this counter.

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setCount(count + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return <p>{count}</p>;
}

Output:

The display gets stuck at 1 and never advances.

The effect runs once, capturing count = 0, so every tick computes 0 + 1. Fix it with the functional updater setCount((c) => c + 1), which never reads a captured value.

How does the functional updater avoid stale closures?

setCount((c) => c + 1) receives the latest committed state as an argument instead of closing over a render-time variable. This lets the effect keep an empty dependency array safely.

Memoization hooks

What is the difference between useMemo and useCallback?

HookReturnsUse case
useMemoA memoized valueSkip expensive recalculations between renders
useCallbackA memoized functionKeep a stable function identity for deps/props

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Both cache based on the dependency array.

When is useCallback actually necessary?

Only when the function’s identity matters: passing it to a React.memo child, or listing it in another hook’s dependency array. Wrapping every handler adds overhead and noise without benefit.

Does useMemo guarantee the value is cached?

No. React may discard memoized values to free memory, recomputing on the next render. Treat useMemo as a performance hint, not a semantic guarantee, and never rely on it for correctness.

useRef and custom hooks

What are the two main uses of useRef?

Holding a reference to a DOM node, and storing a mutable value that persists across renders without triggering one. Unlike state, mutating ref.current does not cause a re-render.

Why use a ref instead of state for a value?

When the value is needed for logic but should not drive the UI, such as a previous prop, a timeout id, or an instance counter. Updating it is synchronous and render-free.

function Timer() {
  const renders = useRef(0);
  renders.current += 1; // does not re-render
  return <p>Render count: {renders.current}</p>;
}

What makes something a custom hook?

A function whose name starts with use and that calls other hooks. It lets you extract and reuse stateful logic. Each call gets its own isolated state; custom hooks share logic, not state.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((v) => !v), []);
  return [on, toggle];
}

Do two components using the same custom hook share state?

No. Calling a custom hook runs its hooks fresh per component, so each instance maintains independent state. To share state, lift it into context or a store, not into the hook itself.

Best practices

  • Let eslint-plugin-react-hooks enforce the rules and exhaustive dependencies; suppress warnings only with a deliberate comment.
  • Prefer functional state updaters in effects and callbacks to sidestep stale closures.
  • Always return cleanup from effects that subscribe, time, or fetch.
  • Reach for useMemo and useCallback only after profiling shows a real cost.
  • Use refs for values that should not trigger renders, and state for values that should.
  • Name custom hooks with the use prefix and keep them focused on one concern.
Last updated June 14, 2026
Was this helpful?