React Hooks Interview Questions and Answers

useState, useEffect, useMemo, useCallback, useRef, useReducer, custom hooks and the rules of hooks, answered the way an interviewer wants to hear them.

18 questions7 fresher, 8 mid-level, 3 senior

Hooks are the densest part of a React interview. Anyone can list them; the interviewer is looking for whether you know when each one is the wrong choice. The two questions that catch most candidates are the dependency array of useEffect and the difference between useMemo and useCallback, so make sure those two are automatic.

A rule of thumb worth saying out loud during the interview: reach for state, then derive everything you can from it, and only add an effect when you need to synchronise with something outside React.

1.What are React hooks and why were they introduced?

Fresher

Hooks are functions that let a function component use state, effects and other React features. They were introduced in React 16.8 to make stateful logic reusable without wrapper components, and to stop related code being split across lifecycle methods.

Before hooks, only classes could hold state, so sharing stateful logic meant higher order components or render props. Both add layers to the tree, and a screen wrapped in five of them became hard to read and to debug.

  • Reusable logic. A custom hook packages behaviour and can be called by any component, with no wrapper.
  • Related code stays together. A subscription's setup and teardown live in one useEffect instead of being split between componentDidMount and componentWillUnmount.
  • No `this`. No binding, no confusion about which instance a callback refers to.
  • Smaller components. With no need for a class, most components are a function and a few hook calls.

Likely follow-up questions

  • Can you use hooks in a class component?
  • Did hooks make classes deprecated?
Practise this in the quiz

2.What are the rules of hooks, and why do they exist?

Fresher

Only call hooks at the top level of a component or another hook, never inside conditions, loops or nested functions, and only call them from React function components or custom hooks. React identifies hooks by call order, so the order must be identical on every render.

React does not know the names of your state variables. It keeps a list per component and hands back the first slot to the first useState call, the second slot to the second, and so on. Skip a call on one render and every hook after it receives the wrong slot.

Why the rule is not arbitrary.
// Broken: on renders where isLoggedIn is false, name gets email's slot
if (isLoggedIn) {
  const [name, setName] = useState('');
}
const [email, setEmail] = useState('');

// Correct: the condition goes inside, not around
const [name, setName] = useState('');
const [email, setEmail] = useState('');

Likely follow-up questions

  • What error do you get if you break the rule?
  • How would you conditionally run an effect then?
Practise this in the quiz

3.How does useState work?

Fresher

useState returns a pair: the current value for this render and a setter. Calling the setter schedules a re-render with the new value; it does not change the variable you are currently holding.

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

// Lazy initial state: the function runs on the first render only
const [rows, setRows] = useState(() => parseHugeCsv(input));

// Functional update: use it whenever the next value depends on the last
setCount((previous) => previous + 1);
  • The initial value is used on the first render only. Passing a different value later does nothing.
  • Pass a function as the initial value when computing it is expensive, otherwise the computation runs on every render and the result is thrown away.
  • React bails out of re-rendering if the new value is Object.is equal to the current one.
  • State must be treated as immutable. Replace objects and arrays rather than mutating them, or the equality check will see no change.

Likely follow-up questions

  • What is lazy initial state?
  • Why does my component not re-render after I update an array?
Practise this in the quiz

4.What is useEffect and when does it run?

Fresher

useEffect runs a function after React has committed the render to the DOM and the browser has painted. It runs after every render by default, or only when a value in the dependency array has changed.

The purpose of an effect is synchronisation with something outside React: a subscription, a timer, a browser API, an analytics call, a non React widget. It is not a general "run this after render" hook, and treating it as one is the single most common source of React bugs.

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id); // cleanup
}, []);

When you do not need an effect

  • Deriving a value from props or state: compute it during render instead.
  • Reacting to a user action: put the logic in the event handler, where you know what happened.
  • Resetting state when a prop changes: give the component a key instead.
  • Transforming data for display: do it in render, memoised if it is genuinely expensive.

Likely follow-up questions

  • What is the difference between useEffect and componentDidMount?
  • Why should the effect callback not be async?
  • How do you cancel a fetch when the component unmounts?
Practise this in the quiz

5.What does the dependency array do, and what changes between no array, an empty array and a populated one?

Fresher

No array means the effect runs after every render. An empty array means it runs once after mount. A populated array means it re-runs whenever one of those values changes by reference or value.

Written asRuns
useEffect(fn)After every render
useEffect(fn, [])Once after mount, cleanup on unmount
useEffect(fn, [a, b])After mount, and after any render where a or b changed

React compares dependencies with Object.is. Objects, arrays and functions created during render are new references every time, so an effect depending on one runs on every render even when nothing meaningful changed.

Likely follow-up questions

  • How would you run an effect only when a value changes, not on mount?
  • What causes an infinite re-render loop with useEffect?
Practise this in the quiz

6.What is the cleanup function in useEffect, and when does it run?

Mid-level

The function you return from an effect. React runs it before the effect runs again, and once more when the component unmounts, so every subscription, timer or listener is undone exactly once.

Cleanup also solves the stale response race in data fetching.
useEffect(() => {
  let cancelled = false;

  fetch(`/api/users/${id}`)
    .then((response) => response.json())
    .then((data) => {
      if (!cancelled) setUser(data);
    });

  return () => {
    cancelled = true;
  };
}, [id]);

Without the flag, switching quickly from user 1 to user 2 can leave user 1's slower response arriving last and overwriting the correct data. AbortController does the same job and also stops the request.

Likely follow-up questions

  • How does StrictMode help you find a missing cleanup?
  • What is a race condition in data fetching?
Practise this in the quiz

7.What is the difference between useEffect and useLayoutEffect?

Mid-level

useEffect runs asynchronously after the browser has painted. useLayoutEffect runs synchronously after the DOM is updated but before paint, so it can measure and adjust layout without the user seeing a flicker.

  • Use useLayoutEffect when you must read layout, such as an element's size or scroll position, and immediately change something based on it, for example positioning a tooltip.
  • Everything else belongs in useEffect. useLayoutEffect blocks paint, so slow work in it delays what the user sees.
  • useLayoutEffect does not run on the server and warns during server rendering, because there is no layout to measure.
useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
  setTooltipTop(height + 8); // applied before the user sees anything
}, [content]);

Likely follow-up questions

  • Which one runs first?
  • What is useInsertionEffect for?
Practise this in the quiz

8.What is useContext, and what are its limitations?

Fresher

useContext reads the nearest provider's value for a context, letting a deep component get data without prop drilling. Its limitation is that every consumer re-renders whenever the provider's value changes, whatever part of the value they actually use.

const ThemeContext = createContext('light');

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>...</div>;
}

<ThemeContext.Provider value={theme}>
  <Toolbar />
</ThemeContext.Provider>
  • The default passed to createContext is only used when there is no provider above the component.
  • Memoise the provider value, or a new object literal on every parent render re-renders every consumer.
  • Split unrelated concerns into separate contexts rather than one large value object.
  • Context is a transport mechanism, not a state manager. It moves a value down the tree; it does nothing about how that value is updated.

Likely follow-up questions

  • Is context a replacement for Redux?
  • How do you stop unnecessary context re-renders?
Practise this in the quiz

9.What is useReducer and when would you use it instead of useState?

Mid-level

useReducer holds state and updates it by dispatching actions to a reducer function. Prefer it when several state values change together, when the next state depends on the previous one in non-trivial ways, or when the update logic is worth testing on its own.

function reducer(state, action) {
  switch (action.type) {
    case 'submit':
      return { ...state, status: 'saving', error: null };
    case 'success':
      return { ...state, status: 'done', data: action.payload };
    case 'failure':
      return { ...state, status: 'idle', error: action.error };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(reducer, { status: 'idle', error: null });
  • The reducer is a pure function of state and action, so it can be unit tested with no React at all.
  • dispatch has a stable identity, so passing it down never invalidates a memoised child.
  • It keeps impossible states out of reach: three related useState calls can be set inconsistently, one reducer transition cannot.
  • It is not a performance optimisation, and it is overkill for a boolean.

Likely follow-up questions

  • How would you combine useReducer with context?
  • Is useReducer the same as Redux?
Practise this in the quiz

10.What is useMemo, and when is it worth using?

Mid-level

useMemo caches the result of a calculation between renders and recomputes it only when its dependencies change. It is worth using for genuinely expensive computations, and for keeping an object or array reference stable so a memoised child or an effect does not fire needlessly.

const sorted = useMemo(
  () => [...rows].sort((a, b) => a.score - b.score),
  [rows]
);

The two legitimate reasons are cost and identity. Cost: the calculation is slow enough to matter, which you should confirm with the Profiler rather than assume. Identity: the value is passed to a React.memo child, used as a dependency, or provided through context, where a new reference each render defeats the point.

Likely follow-up questions

  • Does useMemo guarantee the value is cached?
  • What does the React Compiler change about this?
Practise this in the quiz

11.What is the difference between useMemo and useCallback?

Mid-level

useMemo caches the value a function returns; useCallback caches the function itself. useCallback(fn, deps) is exactly useMemo(() => fn, deps).

const value = useMemo(() => computeTotal(items), [items]);   // caches a number
const onSave = useCallback(() => save(id), [id]);            // caches a function

// Equivalent to the line above
const onSave = useMemo(() => () => save(id), [id]);

useCallback exists because functions are recreated on every render, and a new function reference breaks React.memo on a child and re-triggers any effect that depends on it. It has no benefit when the function is passed to a plain DOM element: onClick on a button does not care that the reference changed.

Likely follow-up questions

  • When does useCallback do nothing?
  • Why is my React.memo component still re-rendering?
Practise this in the quiz

12.What is useRef, and what are its two distinct uses?

Mid-level

useRef returns a mutable object with a .current property that survives re-renders and never triggers one. It is used to reach a DOM node, and to store a mutable value that the UI does not display, such as a timer id or the previous value of a prop.

// 1. A DOM node
const inputRef = useRef(null);
<input ref={inputRef} />;
inputRef.current.focus();

// 2. An instance variable that does not belong in state
const timerRef = useRef(null);
timerRef.current = setTimeout(save, 500);

The decision rule interviewers want: if changing it should update the screen, it is state; if it should not, it is a ref. Writing to a ref during render is also not allowed, because it makes the render impure. Read and write refs in effects and event handlers.

Likely follow-up questions

  • What is the difference between useRef and a variable declared with let in the component?
  • How would you store the previous value of a prop?
Practise this in the quiz

13.What is a custom hook, and when should you write one?

Mid-level

A custom hook is a function whose name starts with use and which calls other hooks. Write one when the same stateful logic appears in more than one component, or when a component's logic is complex enough that naming it makes the component readable.

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

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

  return debounced;
}
  • The use prefix is not decoration: the lint rules and the React Compiler rely on it to know the rules of hooks apply.
  • Custom hooks share logic, not state. Two components calling useDebouncedValue get two independent pieces of state.
  • A custom hook can call other custom hooks, and usually should rather than duplicating them.
  • Return whatever shape reads best: a value, a tuple like useState, or an object when there are more than two things.

Likely follow-up questions

  • Do two components calling the same custom hook share state?
  • How would you test a custom hook?
Practise this in the quiz

14.What is useTransition, and how does it differ from useDeferredValue?

Senior

Both mark work as non-urgent so React can keep the interface responsive. useTransition wraps the state update you are making and gives you an isPending flag; useDeferredValue takes a value you were given and lets you lag behind it.

// You own the update: mark it as a transition
const [isPending, startTransition] = useTransition();

function onChange(event) {
  setQuery(event.target.value);              // urgent: the input must keep up
  startTransition(() => setResults(search(event.target.value))); // can wait
}

// You only receive the value: defer it
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(deferredQuery), [deferredQuery]);

React can interrupt and restart transition work when something urgent arrives, so typing never stalls behind an expensive list render. Neither hook makes the slow work faster; they change when it blocks the user.

Likely follow-up questions

  • What is concurrent rendering?
  • Can you mark a state update inside a promise as a transition?
Practise this in the quiz

15.What is useId for?

Mid-level

useId generates a unique, stable id that matches between the server render and the client hydration. It exists for accessibility attributes such as linking a label to an input, not for keys or database ids.

function Field({ label, ...props }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} {...props} />
    </>
  );
}

A counter or Math.random() produces different values on the server and the client, which causes a hydration mismatch. One useId call can also seed several related ids by suffixing it, for example ` ${id}-error `.

Likely follow-up questions

  • Why not use Math.random()?
  • Can useId be used as a list key?
Practise this in the quiz

16.What is useSyncExternalStore and when would you need it?

Senior

It subscribes a component to a store that lives outside React, guaranteeing a consistent snapshot during concurrent rendering. Library authors need it; application code usually only meets it indirectly through Redux, Zustand or a similar store.

const isOnline = useSyncExternalStore(
  (callback) => {
    window.addEventListener('online', callback);
    window.addEventListener('offline', callback);
    return () => {
      window.removeEventListener('online', callback);
      window.removeEventListener('offline', callback);
    };
  },
  () => navigator.onLine,       // client snapshot
  () => true                     // server snapshot
);

The problem it solves is tearing: with concurrent rendering, an external value can change halfway through a render, leaving two parts of the same screen showing different values. useSyncExternalStore forces a consistent read. The subscribe function must be stable, so define it outside the component or memoise it, otherwise React resubscribes on every render.

Likely follow-up questions

  • What is tearing?
  • Why can't you just use useEffect and useState?
Practise this in the quiz

17.Which hooks did React 19 add, and what are they for?

Senior

React 19 added use, useActionState, useOptimistic and useFormStatus. Together they cover reading a promise or context during render, and handling form submissions with pending, optimistic and error states without hand-rolled state.

  • `use`. Reads a promise or a context during render. Unlike other hooks it may be called conditionally, and suspending on a promise integrates with the nearest Suspense boundary.
  • `useActionState`. Wraps an async action and returns the last result, a wrapped action for the form, and a pending flag.
  • `useOptimistic`. Shows the expected result immediately and reverts automatically if the action fails.
  • `useFormStatus`. Lets a component inside a form read that form's pending state without prop drilling, which is how a shared submit button knows to disable itself.
const [optimisticLikes, addOptimisticLike] = useOptimistic(
  likes,
  (current) => current + 1
);

async function like() {
  addOptimisticLike();
  await sendLike();
}

Likely follow-up questions

  • How is use different from every other hook?
  • What are Server Actions?
Practise this in the quiz

18.Can you call a hook inside a condition, a loop or a callback?

Fresher

No, with one exception: React 19's use can be called conditionally. Every other hook must be called unconditionally at the top level, because React matches hooks to their stored state by call order.

If the number or order of hook calls differs between two renders of the same component, React throws "Rendered fewer hooks than expected" or silently hands back another hook's state.

Move the condition inside the hook, not around it.
// Wrong
if (userId) {
  useEffect(() => { load(userId); }, [userId]);
}

// Right
useEffect(() => {
  if (!userId) return;
  load(userId);
}, [userId]);

The same applies to rendering a variable number of hooks in a loop. If you need one piece of state per item, render one component per item and let each component own its own state.

Likely follow-up questions

  • How would you handle one piece of state per list item?
Practise this in the quiz

All React interview questions