Rendering • Hooks • Effects • Performance • Testing • 2026

React Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 35 min read

This page is for anyone facing a React round, from a first frontend job to a senior role. Most React interviews open with how rendering works, keys, and props versus state, then spend a long time on hooks and useEffect, because that is where real bugs live. After that come re-renders and memoisation, context versus a state library, error boundaries, lazy loading and a light look at server components, and finally how you test components. Senior rounds add a production story and a judgement call. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer you can say out loud.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Rendering 2 questions

Easy Technical round Fresher, Mid-level Practice question

1. What is the virtual DOM, and what does React actually do between a state change and the screen updating?

What the interviewer is really testing:
Whether you understand render and commit as separate steps, and that React changes only what differs, rather than repeating that the virtual DOM is simply faster.
Answer frame:

Render: components run and return elements, plain objects that describe the UI.

Reconcile: React compares the new tree with the previous one to find what changed.

Commit: only the real DOM nodes that differ are updated.

Sample spoken answer:

"When state changes, React calls the component again, and it returns a new tree of elements. Those elements are just plain objects describing what the UI should look like, and that tree is what people call the virtual DOM. React then compares it with the tree from the last render. That comparison is reconciliation. It uses a couple of simple rules: if an element's type changes, say a div becomes a span, it throws that part away and builds it fresh; if the type is the same, it keeps the DOM node and just updates the props that changed; and for lists it matches children by key. Finally, in the commit phase, it applies only those differences to the real DOM. So rendering a component doesn't mean touching the DOM. It means working out what, if anything, needs to change."

Red flag to avoid:

Saying the virtual DOM is always faster than the real DOM, or that every render rewrites the page.

They may ask next:
  • If a component renders but returns exactly the same output, does the real DOM change?
  • What happens to the state of a child when its element type changes between renders?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

2. Why does React want a key on every item in a list, and what goes wrong if you just use the array index?

What the interviewer is really testing:
Whether you know keys are about identity across renders, and can describe the real bug index keys cause when a list is reordered or filtered.
Answer frame:

Purpose: keys tell React which item is which between renders, so it can match old and new children.

Index problem: when items are inserted, removed or sorted, the index moves to a different item and state follows the wrong row.

Good keys: a stable id from the data, unique among siblings, never generated during render.

Sample spoken answer:

"A key is how React tells list items apart from one render to the next. Without it, React matches children by position. If I use the index as the key and then delete the first item, every item's index shifts by one, so React thinks item two is now item one. That's mostly a wasted update, but it becomes a real bug when rows hold their own state, like a half-typed input or a checked box. The state stays attached to the index, so it jumps to the wrong row. The fix is a stable id from the data, like a database id. Keys only need to be unique among siblings, not across the whole app. And I'd never use something like Math.random in render, because then every item looks brand new every time and React remounts all of them."

Code:
// Stable id from the data, not the position
{todos.map((todo) => (
  <TodoRow key={todo.id} todo={todo} />
))}
Red flag to avoid:

Saying keys only exist to silence a console warning, or that the index is always fine.

They may ask next:
  • When is using the index as a key actually fine?
  • How can changing a key on purpose be used to reset a component's state?
Say it in 60 seconds

State & Props 5 questions

Easy Technical round Fresher Practice question

3. What's the difference between props and state, and which component is allowed to change each one?

What the interviewer is really testing:
Whether you understand one-way data flow: who owns data, who can change it, and how a child asks for a change.
Answer frame:

Props: passed in by the parent, read-only inside the child.

State: owned by the component, changed only through its setter, which triggers a re-render.

Flow: data goes down as props; changes go up through callback props.

Sample spoken answer:

"Props are the inputs a parent passes to a child, like arguments to a function. The child reads them but never changes them. State is data a component owns and remembers between renders, and it changes it only by calling the setter, which tells React to render again. So if a child needs to change something that came in as a prop, it can't edit the prop. The parent passes down a callback, like onChange, the child calls it, and the parent updates its own state. That new value flows back down as a prop. The rule I use to decide is simple: if a value can be worked out from props or other state, it shouldn't be state at all. And if two components need the same changing value, it belongs in whichever parent they share."

Red flag to avoid:

Saying a child can update its props, or treating state as a place to store anything, including values that can be derived.

They may ask next:
  • Is it ever a good idea to copy a prop into state?
  • What happens if a child mutates an object it received as a prop?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

4. A click handler calls setCount(count + 1) three times in a row, but the counter only goes up by one. Why, and how do you fix it?

What the interviewer is really testing:
Whether you know state is a snapshot per render and updates are queued, and when to use the updater function form.
Answer frame:

Snapshot: count is a fixed value for this render; it doesn't change until the next render.

Queue: the three calls each ask for the same value, zero plus one.

Fix: pass an updater, setCount(c => c + 1), which gets the latest queued value.

Sample spoken answer:

"Inside one render, count is just a constant. If it's zero, all three calls say set the count to zero plus one, so React queues the same value three times and the next render shows one. Calling the setter doesn't change the variable right away. It asks React for a new render with a new value. React also batches these calls, so they produce a single re-render instead of three. The fix is the updater form: setCount(c => c + 1). React runs each updater in order against the latest pending value, so zero becomes one, then two, then three. I use the updater form whenever the new state depends on the old one, especially inside timeouts, intervals or async code, where the count variable I captured might already be out of date."

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

  function addThree() {
    setCount((c) => c + 1);
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  }

  return <button onClick={addThree}>{count}</button>;
}
Red flag to avoid:

Saying setState is broken or random, or suggesting a setTimeout between the calls as the fix.

They may ask next:
  • If you log count right after the three calls, what does it print?
  • Are updates inside a setTimeout or a promise batched too?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

5. A teammate pushes a new item onto an array in state and then calls the setter with the same array. The list doesn't update. What's going on?

What the interviewer is really testing:
Whether you know React detects changes by reference, and can update arrays and objects without mutating them.
Answer frame:

Reference check: React compares the old and new state with Object.is; the same array means no change.

Knock-on bugs: memoised children and effects keyed on that array also miss the change.

Fix: build a new array or object with spread, map or filter.

Sample spoken answer:

"React decides whether state changed by comparing the old and new values by reference. Push changes the array in place, so when my teammate passes that same array to the setter, React sees the identical reference and can skip the re-render. Even when something else causes a render and the new item shows up, it's fragile: memoised children and effects that depend on that array still think nothing changed. The fix is to treat state as read-only and always create a new value. To add, I spread into a new array. To update one item, I map and return a new object for the one that changed. To remove, I filter. If the data is deeply nested and the spreading gets ugly, a helper like Immer lets you write mutating-style code that produces a fresh copy."

Code:
// Wrong: same array, React may skip the update
todos.push(newTodo);
setTodos(todos);

// Right: always a new array
setTodos([...todos, newTodo]);
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
setTodos(todos.filter((t) => t.id !== id));
Red flag to avoid:

Fixing it with a forceUpdate or a dummy counter instead of stopping the mutation.

They may ask next:
  • Does a spread copy nested objects too, or only the top level?
  • Why does mutation also break React.memo on a child that receives the array?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

6. Two sibling components need the same piece of data, and one of them changes it. How do you share it between them?

What the interviewer is really testing:
Whether you know to lift state to the closest common parent, and when that stops scaling.
Answer frame:

Lift: move the state to the nearest parent both siblings share.

Pass down: give one sibling the value and the other a callback to change it.

When it gets deep: use composition or context instead of threading props through many layers.

Sample spoken answer:

"I move the state up to the closest parent they both sit under. Say I have a search box and a results list. The parent holds the query in state, passes the query to the list, and passes a callback like onQueryChange to the search box. When the user types, the box calls the callback, the parent updates its state, and both children render with the new value. There's now one source of truth, so the two can never disagree. The cost is that the parent re-renders on every change, which is usually fine. If the shared parent is many levels up and I'm passing props through components that don't use them, I'd first try composition, passing the finished child down as children, and then reach for context if that's still messy."

Red flag to avoid:

Keeping a copy of the value in each sibling and syncing them with effects.

They may ask next:
  • What is prop drilling, and at what point does it become a real problem?
  • How would composition with children remove a few layers of props?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

7. What's the difference between a controlled and an uncontrolled input in React, and when would you pick each?

What the interviewer is really testing:
Whether you know who owns the input's value in each approach, and the trade-off between control and simplicity.
Answer frame:

Controlled: React state holds the value; the input shows it and reports changes through onChange.

Uncontrolled: the DOM holds the value; you set defaultValue and read it with a ref or form data.

Choice: controlled for live validation or dependent fields; uncontrolled for simple forms and file inputs.

Sample spoken answer:

"In a controlled input, React owns the value. I keep it in state, pass it as value, and update state in onChange. Every keystroke goes through React, so I can validate as the user types, format the text, disable the submit button, or show another field based on the answer. In an uncontrolled input, the browser owns the value. I give it a defaultValue and read what's there when I need it, with a ref or by reading the form data on submit. That's less code and fewer renders, and it's the only option for a file input, because only the user can choose its file. I use controlled inputs when the UI reacts to what's typed, and uncontrolled for simple forms that only matter on submit. Form libraries often lean on uncontrolled inputs for speed on large forms."

Code:
// Controlled
const [email, setEmail] = useState('');
<input value={email} onChange={(e) => setEmail(e.target.value)} />

// Uncontrolled
<input name="email" defaultValue="" ref={emailRef} />
Red flag to avoid:

Saying uncontrolled inputs are wrong, or not knowing who holds the value in each case.

They may ask next:
  • Why does React warn that a component is changing an uncontrolled input to be controlled?
  • What happens if you set value on an input but forget onChange?
Say it in 60 seconds

Effects 6 questions

Easy Technical round Fresher, Mid-level Practice question

8. Walk me through useEffect with no dependency array, an empty array, and an array with values. When does the cleanup run?

What the interviewer is really testing:
Whether you can predict exactly when an effect and its cleanup run, which is the base for debugging almost every effect bug.
Answer frame:

No array: runs after every render.

Empty array: runs after the first render; cleanup runs on unmount.

With values: runs after the first render and again whenever any listed value changes.

Cleanup: runs before the effect runs again and when the component unmounts.

Sample spoken answer:

"With no array, the effect runs after every render. With an empty array, it runs once after the component first appears, and its cleanup runs when the component goes away. With values in the array, it runs after the first render and then again whenever any of those values has changed since the last render. React checks that with Object.is, so a new object or function counts as changed every time. The cleanup is the function I return. It runs before the effect runs again, and on unmount, so it's where I clear timers, remove listeners or cancel a request. The important rule is that the array isn't a setting I choose freely. It should list every prop, state value or function from the component that the effect reads, and the lint rule helps enforce that."

Red flag to avoid:

Treating the array as a way to control how often the effect runs and leaving out values the effect actually uses.

They may ask next:
  • Why might your effect run twice when the component first mounts during development?
  • How is useLayoutEffect different in when it runs?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

9. This component keeps fetching and re-rendering forever. The effect's only dependency is an options object. Find the bug and fix it.

What the interviewer is really testing:
Whether you can spot a dependency that is new on every render, and fix the cause rather than hiding it.
Answer frame:

Cause: the options object is created fresh each render, so the dependency always looks changed.

Loop: effect runs, sets state, component re-renders, new object, effect runs again.

Fix: depend on the primitive value, or create the object inside the effect.

Sample spoken answer:

"The options object is built in the body of the component, so every render makes a brand new object. React compares dependencies by reference, so to React the options always changed and the effect runs after every render. The effect fetches and calls setUser with a new object, which triggers a render, which makes a new options object, and round it goes. The fix is to depend on what really matters, which is the userId, a plain string. I build the options inside the effect, so the effect only runs again when the id changes. If an object really does have to come from outside, I'd memoise it with useMemo so it keeps the same reference. What I wouldn't do is just delete it from the array, because then the effect reads stale values and the lint rule is right to complain."

Code:
// Bug: a new object every render, so the effect never settles
const options = { id: userId };
useEffect(() => {
  fetchUser(options).then(setUser);
}, [options]);

// Fix: depend on the primitive, build the object inside
useEffect(() => {
  fetchUser({ id: userId }).then(setUser);
}, [userId]);
Red flag to avoid:

Removing the dependency or adding a boolean flag to stop the second run without explaining why the object changes.

They may ask next:
  • Would the loop still happen if the fetched data were a number that never changed?
  • How would you spot this kind of loop quickly in the browser?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

10. An interval set up in useEffect with an empty array should add one every second, but the count sticks at 1. Explain why and give two fixes.

What the interviewer is really testing:
Whether you understand stale closures: a callback created once keeps seeing the values from the render it was created in.
Answer frame:

Closure: the interval callback was created on the first render and captured count as 0.

Result: every tick sets the count to 0 plus 1, so it stays at 1.

Fix one: use the updater form, setCount(c => c + 1), so nothing stale is read.

Fix two: if you must read the latest value, keep it in a ref or list it as a dependency.

Sample spoken answer:

"The effect runs once, on the first render, and creates the interval callback then. That callback is a closure over the count from that render, which is zero. Later renders make new count values, but the interval is still holding the old function, so every second it says set the count to zero plus one. The count goes to one and stays there. That's a stale closure. The cleanest fix here is the updater form: setCount(c => c + 1). The callback no longer reads count at all, React hands it the latest value. Another fix is to put count in the dependency array, which works but tears down and restarts the interval on every tick. And when the callback needs to read something fresh, not just update state, I store it in a ref that I keep current, and read ref.current inside the interval."

Code:
useEffect(() => {
  const id = setInterval(() => {
    // setCount(count + 1) would read a stale count
    setCount((c) => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
Red flag to avoid:

Saying setInterval is unreliable in React, or fixing it by moving the interval outside the component.

They may ask next:
  • Where else have you seen stale closures, for example in event listeners or socket handlers?
  • Why is reading a ref inside the callback safe when reading state isn't?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

11. A profile component fetches data in useEffect whenever its id prop changes. Click between users fast and it sometimes shows the wrong person. Why, and how do you fix it?

What the interviewer is really testing:
Whether you can spot a race between overlapping requests and use effect cleanup to ignore or cancel the stale one.
Answer frame:

Race: two requests are in flight; the older one can finish last and overwrite the newer result.

Cleanup: when the id changes, the old effect's cleanup runs, so mark it stale or abort it.

In practice: a data-fetching library handles this, plus caching and retries.

Sample spoken answer:

"Each id change starts a new request, but nothing stops the old one. If I click user one and then user two, and user one's request is slower, it finishes last and calls setUser with the wrong person. The fix uses the cleanup function. When the id changes, React runs the previous effect's cleanup before starting the new one, so I set a flag there, and the old request checks the flag before it sets state. Even better, I pass an AbortController signal to fetch and abort in the cleanup, which cancels the network call too. That's also why the effect running twice in development doesn't cause trouble: the first run is cleaned up. In a real app I'd usually let a data-fetching library own this, since it handles races, caching and retries, but I want to know what it's doing underneath."

Code:
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((res) => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    })
    .then(setUser)
    .catch((err) => {
      if (err.name !== 'AbortError') setError(err);
    });
  return () => controller.abort();
}, [id]);
Red flag to avoid:

Not seeing the race at all, or fixing it with a delay or a debounce alone.

They may ask next:
  • What does the user see between clicking a new user and the data arriving, and how would you handle that state?
  • Why not just disable the list while a request is loading?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

12. When should you not reach for useEffect? Give me a few cases where people use it and shouldn't.

What the interviewer is really testing:
Whether you see effects as a way to sync with things outside React, not a general tool for reacting to state changes.
Answer frame:

Purpose: effects sync with external systems like the network, timers, subscriptions or a non-React widget.

Derived values: compute them during render instead of storing them and syncing with an effect.

User actions: logic that happens because of a click belongs in the event handler.

Resetting state: change the key rather than resetting state in an effect.

Sample spoken answer:

"An effect is for keeping a component in sync with something outside React: a subscription, a timer, a browser API, a request. The most common misuse is derived state. If I have a list and a filter, I don't store filteredItems in state and update it in an effect. I just compute it during render, and wrap it in useMemo if it's genuinely expensive. That removes an extra render and a whole class of out-of-sync bugs. The second misuse is putting event logic in an effect, like watching a submitted flag to send a request. If it happens because the user clicked, it goes in the click handler. The third is resetting a form when a prop like userId changes. Instead of an effect that clears each field, I give the component key={userId}, and React gives it fresh state."

Red flag to avoid:

Saying effects are the right place for any code that should run when state changes.

They may ask next:
  • How would you explain the difference between an event and a synchronisation to a junior developer?
  • Is fetching data on mount a valid use of an effect?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

13. A teammate added a comment to turn off the exhaustive-deps lint warning on an effect, because adding the missing dependency made it run too often. What do you do?

What the interviewer is really testing:
Whether you treat the lint warning as a sign of a real bug and know the proper ways to make an effect's dependencies stable.
Answer frame:

Why it matters: a missing dependency means the effect reads stale values sooner or later.

Find the cause: work out why the dependency changes so often.

Fix options: move the function inside the effect, use an updater, memoise the value, or depend on a primitive.

Question the effect: sometimes the code belongs in an event handler instead.

Sample spoken answer:

"I'd ask to look at it together, because that warning is usually right. If the effect uses a value that isn't in the list, it will read a stale copy at some point, and those bugs are painful to find later. The real question is why the dependency changes so often. Most of the time it's a function or object recreated on every render. If the function is only used by the effect, I move it inside the effect. If the effect sets state from the previous state, the updater form means it doesn't need that state as a dependency. If it's an object, I depend on the primitive fields or memoise it. And sometimes the honest answer is that the code shouldn't be an effect at all, because it responds to a click. I'd keep the disable comment only if there was a clear written reason, which is rare."

Red flag to avoid:

Approving the disable comment because the tests pass, or adding a ref to dodge every dependency without understanding the cause.

They may ask next:
  • The dependency is a callback prop from the parent. How do you keep the effect stable then?
  • Would you make this lint rule an error for the whole team?
Say it in 60 seconds

Performance 4 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

14. What's the difference between useMemo and useCallback, and when does using them actually make anything faster?

What the interviewer is really testing:
Whether you know what each hook caches and the specific situations where that cache pays off.
Answer frame:

useMemo: caches the result of a calculation until its dependencies change.

useCallback: caches a function itself; it's useMemo returning a function.

When it helps: an expensive calculation, a prop to a memoised child, or a value used as a hook dependency.

Cost: comparing dependencies and extra code; no gain if nothing downstream checks the reference.

Sample spoken answer:

"useMemo runs a function and keeps its result, and only reruns it when a dependency changes. useCallback keeps the function itself, so I get the same function reference between renders. Really, useCallback is just useMemo that returns a function. Neither makes a component render less on its own. They help in three situations. First, a calculation that's actually slow, like sorting or filtering thousands of rows, which I'd confirm by measuring. Second, passing an object or function to a child wrapped in React.memo, because otherwise a new reference each render defeats the memo. Third, when the value is a dependency of another hook, like an effect, so the effect doesn't rerun for no reason. Outside those cases they only add cost and noise. React also treats the cache as an optimisation, so my code must still be correct without it."

Red flag to avoid:

Saying useCallback stops a function from being created, or that memoising everything is always faster.

They may ask next:
  • Your child component isn't wrapped in React.memo. Does useCallback on its onClick prop help at all?
  • How would you check whether a calculation is slow enough to be worth memoising?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

15. What actually causes a React component to re-render, and how does wrapping it in React.memo change that?

What the interviewer is really testing:
Whether you have a correct model of re-renders, including that a parent re-render re-renders children regardless of props.
Answer frame:

Triggers: the component's own state changes, its parent re-renders, or a context it reads changes.

Props myth: without memo, children re-render when the parent does, even with identical props.

React.memo: skips the re-render if every prop is equal by reference; its own state and context still trigger it.

Sample spoken answer:

"There are three triggers. The component's own state changes, a context value it reads changes, or its parent re-renders. That last one surprises people: by default, when a parent renders, every child renders too, whether its props changed or not. Props changing isn't really a separate cause, it's a side effect of the parent rendering. React.memo changes that for one component. It compares each prop with the previous one using Object.is, and if they're all the same it skips rendering that component and its subtree. The catch is references. If the parent passes an inline object, an inline arrow function, or JSX as children, those are new every time, so memo sees a change and does nothing useful. That's where useMemo and useCallback come in. And a memoised component still re-renders when its own state or a context it uses changes."

Red flag to avoid:

Saying a component only re-renders when its props change.

They may ask next:
  • Is a re-render always a problem? What's the difference between rendering and updating the DOM?
  • How could you restructure the component tree so the expensive child doesn't re-render, without memo at all?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

16. In code review, a teammate has wrapped every function in useCallback and every value in useMemo, saying it's for performance. How do you respond?

What the interviewer is really testing:
Whether you can push back with reasons and evidence while keeping the conversation constructive.
Answer frame:

Acknowledge: the intent is good, and memoising has real uses.

Explain the cost: it adds work, dependency lists to maintain, and harder-to-read code.

Point to where it helps: memoised children, expensive calculations, hook dependencies.

Agree a rule: measure with the profiler first, then write down a team guideline.

Sample spoken answer:

"I'd start by saying the goal is right, and there are places where it matters. Then I'd explain the trade-off. useMemo and useCallback aren't free: React has to store the value and compare dependencies every render, and every hook adds a dependency list someone has to keep correct. Get one wrong and you've swapped a speed problem for a stale data bug. Most of these calls protect nothing, because the child isn't wrapped in React.memo and nothing else checks the reference. I'd suggest we keep the ones that pass props to a memoised child, feed an effect, or wrap a genuinely heavy calculation, and drop the rest. Then I'd offer to pair on profiling the screen, because if there is a slowdown we want to fix the real cause. Afterwards I'd write the rule down so reviews stay consistent."

Red flag to avoid:

Either approving it without comment or telling the teammate memoisation is always bad.

They may ask next:
  • What if your teammate says it can't hurt, so why not keep them?
  • How would a tool that memoises automatically change this conversation?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

17. Users say a page freezes while they type in a search box that filters a list of several thousand rows. What do you do?

What the interviewer is really testing:
Whether you diagnose before fixing and know the main tools: keeping input urgent, deferring heavy updates, memoising and virtualising.
Answer frame:

Reproduce and profile: is the time spent filtering, rendering rows, or both?

Keep typing urgent: the input updates immediately; the list uses useDeferredValue or a transition.

Do less work: memoise the filtered result and the row component.

Render less: virtualise the list so only visible rows are in the DOM.

Sample spoken answer:

"First I'd reproduce it and profile a few keystrokes to see where the time goes: the filter itself, rendering thousands of rows, or both. Usually it's the rendering. My first fix is to stop the list from blocking the input. The input keeps its own state so every keystroke shows up at once, and the list reads a deferred copy of the query through useDeferredValue. React then treats updating the list as lower priority and can drop work that's already out of date while the user keeps typing. Second, I'd memoise the filtered array on the deferred query, and wrap the row component in React.memo so unchanged rows skip rendering. Third, if there are still thousands of rows on screen, I'd virtualise the list so only the rows in view exist in the DOM. If the filtering happened on the server, I'd debounce the requests too. Then I'd profile again to confirm."

Red flag to avoid:

Jumping straight to a debounce or to memo without measuring where the time actually goes.

They may ask next:
  • How is useDeferredValue different from debouncing the input?
  • What trade-offs does virtualising a list bring for accessibility and browser find-in-page?
Say it in 60 seconds

Hooks 3 questions

Easy Technical round Fresher, Mid-level Practice question

18. What is useRef used for, and how is a ref different from a piece of state?

What the interviewer is really testing:
Whether you know refs hold values that persist without causing renders, and when that's the right choice over state.
Answer frame:

What it is: a box with a current property that lives for the whole life of the component.

Difference: changing ref.current doesn't re-render; changing state does.

Uses: DOM nodes for focus or measuring, timer ids, the latest value for a callback.

Sample spoken answer:

"useRef gives me an object with a current property, and React hands me the same object on every render. I can change current whenever I want and it doesn't trigger a render. State is the opposite: changing it tells React to render again with the new value. So the question I ask is, does the screen need to show this value? If yes, it's state. If it's just something I need to remember behind the scenes, it's a ref. The classic use is a DOM node: I pass the ref to an input and call ref.current.focus() after a button click. Others are storing a timer id so I can clear it later, or keeping the latest value for an interval callback. One rule: I don't read or write ref.current during render, only in effects and event handlers, or the output becomes unpredictable."

Code:
function SearchBox() {
  const inputRef = useRef(null);
  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Search</button>
    </>
  );
}
Red flag to avoid:

Using a ref to hold a value that's shown on screen and then wondering why the screen doesn't update.

They may ask next:
  • Why is ref.current null during the first render?
  • How would you track the previous value of a prop?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

19. Why can't you call a hook inside an if statement or a loop? What would actually break?

What the interviewer is really testing:
Whether you understand that React matches hooks to their stored state by call order, not by name.
Answer frame:

How React tracks hooks: by the order they're called in each render.

What breaks: skip one call and every later hook reads the state meant for another.

The rule: call hooks only at the top level of a component or custom hook; put conditions inside the hook.

Sample spoken answer:

"React doesn't know hooks by name. For each component it keeps a list of hook slots, and on every render it hands them out in order: the first useState call gets slot one, the second gets slot two, and so on. If I put a hook inside an if, then on a render where the condition is false that call is skipped, and every hook after it gets shifted by one. Suddenly a useState reads the value that belonged to a different useState, or an effect gets someone else's dependencies. In development React usually warns or throws an error about the order or number of hooks. So hooks go at the top level of a component or custom hook, never inside conditions, loops, or nested functions. If I need conditional behaviour, I call the hook every time and put the condition inside it, like an early return inside the effect."

Red flag to avoid:

Saying it's just a style convention, or that React tracks hooks by variable name.

They may ask next:
  • How does an early return above a hook cause the same problem?
  • What does the ESLint hooks plugin check for you?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

20. Write a custom hook that debounces a fast-changing value, like search box text, before you use it to fetch results.

What the interviewer is really testing:
Whether you can package effect logic into a reusable hook with correct cleanup, and know what a custom hook shares and what it doesn't.
Answer frame:

State: keep the debounced copy in state, starting at the current value.

Effect: on each change, start a timer to copy the value; clear it in the cleanup.

Reuse: each component that calls the hook gets its own state; hooks share logic, not data.

Sample spoken answer:

"The hook takes a value and a delay and returns a copy of the value that only updates once the user has stopped typing for that long. Inside, I keep the debounced value in state. An effect depends on the value and the delay. Each time the value changes, it starts a timeout that copies it across, and the cleanup clears that timeout. So while someone is typing quickly, every keystroke cancels the previous timer, and only the last one survives. In the component, the input stays fully responsive because it still uses the raw text, and the fetch effect depends on the debounced value instead. The name starts with use, which lets the lint rules check it. And if two components use this hook, they each get their own separate state. A custom hook shares the logic, not the data."

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

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

  return debounced;
}

// In a component
const debouncedQuery = useDebouncedValue(query, 300);
Red flag to avoid:

Forgetting the cleanup, so every keystroke still fires an update later.

They may ask next:
  • How would you test this hook without waiting for real time to pass?
  • Would you still need to handle out-of-order responses in the fetch that uses it?
Say it in 60 seconds

State Management 2 questions

Medium Technical round Mid-level, Senior Practice question

21. How does React context work, and when is it the wrong tool for sharing data?

What the interviewer is really testing:
Whether you know context is a way to pass values down, not a state manager, and understand how it causes re-renders.
Answer frame:

Mechanism: a Provider supplies a value; any component below that reads it with useContext gets it without props.

Re-renders: when the value changes, every component that reads that context re-renders.

Good fit: values that change rarely, like theme, locale or the signed-in user.

Wrong fit: fast-changing data read by many components; split contexts or use a store.

Sample spoken answer:

"Context lets a parent make a value available to anything below it, without passing props through every layer. I create a context, wrap part of the tree in its Provider with a value, and any component inside can read it with useContext. It doesn't manage state by itself. Usually the value comes from useState in the provider component. The thing to watch is re-renders. When the provider's value changes, every component reading that context re-renders, even if it only uses one field. And if I pass a new object as the value on every render, consumers re-render every time the provider does. So I memoise the value and split contexts by how often they change. It's great for theme, language or the current user. It's the wrong tool for something like fast form input or a big shared data set read in many places."

Red flag to avoid:

Calling context a replacement for any state library, with no mention of the re-render cost.

They may ask next:
  • How would you split one large app context so a change to one field doesn't re-render everything?
  • What happens if a component calls useContext but there's no Provider above it?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

22. When would you add a state management library to a React app instead of using useState and context? How do you decide?

What the interviewer is really testing:
Whether you separate server data from client state and pick tools by the problem, not by habit.
Answer frame:

Local first: most state belongs in the component that uses it, or its nearest shared parent.

Server data: cached API data is its own problem; a data-fetching library handles caching, refetching and loading states.

Shared client state: a store with selectors lets components subscribe to just the slice they use.

Decide on pain: add a library when you can name the problem it fixes.

Sample spoken answer:

"I start by sorting the state into kinds. Most UI state, like whether a dropdown is open, stays local. Then there's data from the server, which is really a cache: it needs loading and error states, refetching, and deduping. A lot of what used to go into global stores is really that, and a data-fetching library like TanStack Query handles it far better than hand-written context. What's left is shared client state, like a multi-step editor or a shopping cart touched from many screens. If it changes often and many components read different parts of it, context re-renders too much, and a store like Redux Toolkit or Zustand fits better, because components subscribe through selectors and only re-render when their slice changes. You also get dev tools and a clear place for update logic. I add one when I can name the pain, not up front."

Red flag to avoid:

Saying every app needs Redux, or that context always replaces one, without talking about server data or re-renders.

They may ask next:
  • What problems appear when you store API responses in a global store by hand?
  • How would you move an app off one large context without a big-bang rewrite?
Say it in 60 seconds

App Structure 3 questions

Medium Technical round Mid-level, Senior Practice question

23. What is an error boundary, what errors does it catch, and which ones does it miss?

What the interviewer is really testing:
Whether you know error boundaries protect rendering only, so you still handle errors in handlers and async code yourself.
Answer frame:

What it is: a class component that shows a fallback when something below it throws.

Catches: errors during rendering, in lifecycle methods and in constructors of its children.

Misses: event handlers, async code like timers and promises, server rendering, and its own errors.

Placement: around routes and risky widgets, so one failure doesn't blank the whole page.

Sample spoken answer:

"An error boundary is a component that catches errors thrown while rendering the components under it and shows a fallback instead of unmounting the whole app. It has to be a class component with getDerivedStateFromError to switch to the fallback, and usually componentDidCatch to log the error. There's no built-in hook version, so most teams use a small library or one shared class. It catches errors during render, in lifecycle methods and in constructors below it. It doesn't catch errors in event handlers, in async code like a timeout or a promise, during server rendering, or inside the boundary itself. For those I use try and catch, and set error state that the UI shows. For placement, I put one near the top as a last resort and more around routes and risky widgets, so a broken chart doesn't take down the page."

Red flag to avoid:

Saying an error boundary catches every error in the app, including clicks and failed requests.

They may ask next:
  • How would you let the user retry after a boundary has caught an error?
  • If an event handler throws, how do you get that error to show in the UI?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

24. How do React.lazy and Suspense help with a large bundle, and where would you split the code in a real app?

What the interviewer is really testing:
Whether you can use code splitting on purpose, with sensible split points and loading states.
Answer frame:

lazy: takes a function that dynamically imports a module and loads that component on first render.

Suspense: shows a fallback while the code is loading.

Split points: routes, heavy modals, editors and charts that most users don't open.

Gotchas: declare lazy components at module level; watch for loading flashes.

Sample spoken answer:

"React.lazy lets me turn a component into its own chunk. I give it a function that calls a dynamic import, and the bundler splits that file out. The code only downloads the first time the component renders. While it's loading, the component suspends, and the nearest Suspense boundary above shows its fallback, like a spinner or a skeleton. The best split points are the ones most users never hit, or not straight away: separate routes, a settings page, a rich-text editor, a charting library, a big modal. I declare lazy components at the top level of a module, never inside another component, or they'd be recreated and lose state on every render. I also place Suspense boundaries so a small part of the page shows a fallback, not the whole screen, and I can start the import early on hover to hide the delay."

Code:
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Settings />
    </Suspense>
  );
}
Red flag to avoid:

Lazy-loading every small component, or defining the lazy component inside the render function.

They may ask next:
  • What happens if the chunk fails to download, say after a new deploy?
  • Besides code, what else can make a component suspend?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

25. At a high level, what are React Server Components, and how are they different from plain server-side rendering?

What the interviewer is really testing:
Whether you understand that server components change where component code runs and what ships to the browser, while SSR only changes where the first HTML is produced.
Answer frame:

Server components: run only on the server; their code and dependencies never ship to the browser.

Limits: no state, effects or browser APIs; interactive parts are client components marked with 'use client'.

SSR: renders client components to HTML on the server, then the same JavaScript loads and hydrates in the browser.

Together: they complement each other; a framework usually wires both up.

Sample spoken answer:

"With classic server-side rendering, the server runs my components to produce the first HTML, so the page shows up fast, but all that component code still ships to the browser and hydrates there to become interactive. Server components are different. They only ever run on the server, so their code and any libraries they import never reach the browser bundle. They can read data directly, like calling the database, and pass the result down. The trade-off is they can't use state, effects, or browser APIs. Anything interactive goes in a client component, marked with the 'use client' directive, and a server component can render client components and pass them props that can be serialised. So SSR is about when the first HTML appears, and server components are about where component code runs. In practice you use them through a framework that supports them."

Red flag to avoid:

Saying server components are just SSR with a new name, or that they can use useState.

They may ask next:
  • Can a client component import a server component directly? How do you nest one inside the other?
  • Why do props passed from a server component to a client component have to be serialisable?
Say it in 60 seconds

Testing 2 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

26. How do you test a React component? What do you assert on, and what do you avoid testing?

What the interviewer is really testing:
Whether you test behaviour the way a user sees it, so tests survive refactors and catch real breakage.
Answer frame:

Tools: a test runner like Jest or Vitest plus React Testing Library with a DOM environment.

Approach: render, find elements by role, label or text, act like a user, assert on what's on screen.

Avoid: asserting on internal state, hook calls or component structure.

Boundaries: mock the network, not your own components.

Sample spoken answer:

"I use React Testing Library with Jest or Vitest. The idea is to test the component the way a user uses it. I render it, find elements the way a person would, by role, label or visible text, then interact with user-event, like typing into a field or clicking a button, and assert on what's on screen afterwards. For a login form I'd check that submitting with an empty email shows an error message, not that some state variable is set. I avoid testing internals like state values, which hooks were called, or the exact component tree, because those break on every refactor even when nothing is wrong for the user. Querying by role has a side benefit: if I can't find a button by its role and name, a screen reader user probably can't either. For network calls I mock at the fetch or HTTP level."

Red flag to avoid:

Testing that setState was called or reading component internals, instead of checking what the user sees.

They may ask next:
  • What's the difference between getBy, queryBy and findBy queries?
  • When would a snapshot test be useful, and when does it become noise?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

27. Write a test for a component that shows 'Loading' and then renders a user's name after a fetch. How do you handle the waiting?

What the interviewer is really testing:
Whether you can test async UI without sleeps or flaky timing, using mocks and the async queries.
Answer frame:

Mock the network: stub fetch to resolve with known data, fresh for each test.

Check loading: assert the loading text right after render.

Wait properly: use an async findBy query, which retries until the element appears or times out.

Also test: the error path, by making the mock reject.

Sample spoken answer:

"First I control the network. I replace fetch with a fresh mock before each test, so tests don't leak into each other, and make it resolve to a response whose json returns a known user. Then I render the component and check the loading text is there straight away, with a getBy query, since it should appear synchronously. For the name, I don't add a sleep. I await a findBy query, which keeps retrying until the element shows up or a timeout passes. Here I look for a heading with the user's name. That's the whole happy path. I'd write a second test where the mock rejects and check the error message shows. If I see act warnings, it usually means something updated after the test ended, which tells me I forgot to wait for something."

Code:
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';

beforeEach(() => {
  global.fetch = jest.fn();
});

test('shows the user after loading', async () => {
  fetch.mockResolvedValue({
    ok: true,
    json: async () => ({ name: 'Asha Rao' }),
  });

  render(<UserProfile id="42" />);

  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  expect(
    await screen.findByRole('heading', { name: 'Asha Rao' })
  ).toBeInTheDocument();
});
Red flag to avoid:

Using a fixed setTimeout in the test to wait for data, or asserting on the component's state instead of the screen.

They may ask next:
  • How would you test that the component ignores a stale response when the id changes quickly?
  • Why is mocking at the HTTP level often better than mocking your own API module?
Say it in 60 seconds

Real Work 3 questions

Medium Behavioral round Mid-level, Senior Practice question

28. Tell me about a React screen that felt slow. How did you find which components were re-rendering too much, and what did you change?

What the interviewer is really testing:
Whether you measure before optimising, use the profiler, and fix the cause rather than sprinkling memo everywhere.
Answer frame:

Symptom: what the user felt and on which screen.

Measure: the React DevTools Profiler, and highlighting renders, to find what rendered and why.

Fix: the specific cause, such as unstable context values, state kept too high, or a huge list.

Proof: profile again and confirm the change with the people who reported it.

Sample spoken answer:

"At my last company we had an analytics dashboard where typing in the filter box lagged badly. Instead of guessing, I recorded a session in the React DevTools Profiler while typing. It showed the whole dashboard, every chart and table, re-rendering on each keystroke. The cause was a single context provider at the top holding the filter text together with the loaded data, and its value was a new object on every render, so every consumer updated. I moved the filter input's state down into the filter bar, so typing only rendered that bar, and applied the filter when the user paused. I also split the context into data and settings and memoised the value. When I profiled again, a keystroke only rendered the input, and typing felt instant. The team that reported it confirmed it the same week."

Red flag to avoid:

A story with no measurement, where the fix was adding useMemo everywhere and hoping.

They may ask next:
  • Why did you move state down rather than wrap the charts in React.memo?
  • How do you stop the same problem coming back in six months?
Say it in 60 seconds
Hard Behavioral round Mid-level, Senior Practice question

29. Tell me about a bug in a React app that came from a useEffect or from stale state. How did you track it down?

What the interviewer is really testing:
Whether you can debug effect behaviour methodically, explain the root cause clearly, and stop it happening again.
Answer frame:

Signal: how the bug showed up, whether from users, logs or monitoring.

Trace: how you narrowed it down to one effect and one dependency.

Root cause: why the effect ran when it shouldn't have, or read an old value.

Prevention: the fix, plus a test or a lint rule so it stays fixed.

Sample spoken answer:

"In a project I worked on, the backend team noticed one endpoint getting far more calls than we had users. In the browser's network tab I saw the orders page firing the same request over and over while it sat open. I found the effect: it depended on a filters object that came from a custom hook, and the hook built a new object on every render. When the response arrived, we set state, the page re-rendered, the hook returned a fresh object, and the effect ran again. The screen looked fine because the data was the same each time, but it kept hitting the server. I fixed the hook to memoise its return value and changed the effect to depend on the actual filter fields. Then I added a test that counts fetch calls after render, and we turned the exhaustive-deps lint rule into an error."

Red flag to avoid:

Blaming React or the backend, or describing a fix that only hides the symptom, like a guard flag.

They may ask next:
  • Why didn't this show up in code review?
  • How would you have caught it before the backend team did?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

30. Tell me about a time you refactored a large or messy React component, or moved class components to hooks. How did you avoid breaking things?

What the interviewer is really testing:
Whether you refactor safely in small steps, with tests guarding behaviour, and can explain why the change was worth it.
Answer frame:

Why: what the old component made hard, such as bugs, slow changes or no tests.

Safety net: behaviour tests written before touching the code.

Steps: small pull requests, pulling logic into custom hooks and child components.

Result: what got easier, and anything you'd do differently.

Sample spoken answer:

"At my last company we had a checkout component, a class component of well over a thousand lines, with pricing, validation and API calls all mixed together. Every change broke something. Before touching it, I wrote tests with Testing Library that covered what a user does: adding a coupon, changing the address, seeing errors. Then I went in small pull requests. First I pulled the pricing logic out into plain functions with their own unit tests. Then I converted the class to a function component and moved the data loading into a custom hook, and split the form sections into child components. Each step shipped separately, so if something broke we knew exactly which change did it. One bug did slip through, a double submit, so I added a test for it before fixing it. The component ended up small enough that new people could change it confidently."

Red flag to avoid:

A big-bang rewrite with no tests, or a story where the only reason was that hooks are newer.

They may ask next:
  • How did you convince the team to spend time on this instead of new features?
  • What was hardest about translating lifecycle methods into effects?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card