Back to Blog

React Interview Question Bank — 2 to 3+ Years Experience (2026)

Roundexa Team 2 Sep 2026
React Interview Question Bank — 2 to 3+ Years Experience (2026)

77 conceptual questions with interview-ready answers, plus 28 coding tasks — one block at the end of every section — and three full machine-coding rounds at the end.

Questions marked ★ Added go beyond the usual question lists floating around — the topics a 2–3 year candidate is actually asked in 2026 that most sources skip. The coding tasks come in three shapes, matching how they are actually asked: Write it — implement a hook or component from scratch; Fix it — a broken snippet where you have to name the bug before fixing it; Refactor it — working code with the wrong shape.

Answers here are deliberately short — the length you should speak, not the length you should know. Expand each one with an example from your own project. An answer delivered word-for-word from a list is obvious within two follow-ups.

Version context: React 19 is the current major version (19.2.x as of mid-2026). React 19 introduced Actions, the use API, ref as an ordinary prop, and stable Server Components. React 19.2 added <Activity> and useEffectEvent. The React Compiler has been stable since late 2025. Interviewers are asking about these now — a React 18-only answer dates you.

1. Hooks in Practice

Q1

Why does useEffect sometimes run twice on mount, and should you 'fix' it?

In development Strict Mode, React intentionally mounts, unmounts and remounts the component to surface effects that are not safe to re-run. It does not happen in production. The correct response is not to disable Strict Mode but to write a cleanup function — if the double run breaks something, the effect had a real bug.

Q2

What happens if you leave a value out of the dependency array?

The effect keeps the value it captured on the render where it was created, so it silently works with stale data and never re-syncs. The react-hooks/exhaustive-deps lint rule catches almost all of these; suppressing it is how the bug reaches production.

Q3

What is a stale closure, and how do you fix it?

A function created in one render 'remembers' that render's props and state forever. It shows up most often in intervals, subscriptions and event handlers registered once. Fix it by adding the dependency, using a functional state update, or holding the latest value in a ref.

Q4

useState or useReducer — when do you switch?

Switch when several pieces of state change together, when the next state depends on the current one in non-trivial ways, or when the same update logic is scattered across many handlers. useReducer centralises the transitions and makes them testable as a pure function.

Q5

Why use setCount(c => c + 1) instead of setCount(count + 1)?

The functional form receives the latest queued state, so it is correct when multiple updates are batched or when the update happens inside an async callback or effect. The direct form reads whatever value that render closed over, which may already be out of date.

Q6

useRef or useState — how do you choose?

If changing the value should update the UI, it is state. If it just needs to survive across renders — a timer id, a DOM node, a previous value, a 'has this already run' flag — it is a ref. Mutating ref.current never triggers a render, which is the whole point.

Q7

useEffect or useLayoutEffect?

useLayoutEffect runs synchronously after DOM mutations and before the browser paints, so use it only when you must measure or adjust the DOM before the user sees it — otherwise you block painting. Default to useEffect; reach for the layout version to prevent a visible flicker.

Q8

useMemo, useCallback and React.memo — what is the difference?

useMemo caches a computed value, useCallback caches a function reference, and React.memo skips re-rendering a component when its props are shallowly equal. The first two exist largely to make the third one work, because a new object or function prop on every render defeats it.

Q9

When do useMemo and useCallback make an app slower?

Always, slightly — they cost memory plus a dependency comparison on every render. They only pay for themselves when the wrapped computation is genuinely expensive or when the stable reference prevents a costly re-render downstream. Wrapping a price * quantity calculation is pure overhead.

Q10

Why can't Hooks be called inside conditions or loops?

React associates hook state with the call order, not with names. If the order changes between renders, the state from one hook lands in another, which produces bugs that look impossible. That is why the rule is 'top level, React functions only.'

Q11

When do you extract a custom Hook, and what does it actually share?

When the same stateful logic — an effect plus its state plus its cleanup — appears in more than one component. It shares the logic, not the state: two components calling useFetch() each get their own independent state, which is the point people most often get wrong in interviews.

Q12 · ★ Added

What problem does useSyncExternalStore solve?

Subscribing to a store outside React — a browser API, a WebSocket, a third-party state library — with useState plus useEffect can tear under concurrent rendering, where different parts of one render read different values. useSyncExternalStore gives React a consistent snapshot. Most people meet it inside state libraries rather than writing it directly.

Q13 · ★ Added

How do you use the latest value inside an effect without re-running the effect?

This is the classic 'I need the newest callback but I don't want to reconnect the socket every render' problem. Before React 19.2 the pattern was a ref updated in an effect; useEffectEvent now expresses it directly — the event function always sees fresh values and is not a dependency.

Coding Tasks — Hooks

C1 · Coding

Write useDebounce(value, delay).

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);   // this line IS the debounce
  }, [value, delay]);
  return debounced;
}

What it tests: whether you understand that the cleanup cancelling the previous timer is what makes it a debounce. Without it you have a delay, not a debounce.

C2 · Coding

This counter logs 0 forever. Fix it, and give two different fixes.

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

Fix A — add count to the dependencies, accepting that the interval is torn down and recreated on every change. Fix B — keep the interval alive and read the latest value from a ref:

const countRef = useRef(count);
useEffect(() => { countRef.current = count; });

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

What it tests: stale closures, and whether you can name the trade-off between the two fixes rather than just producing one.

C3 · Coding

Write usePrevious(value).

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;   // effect runs after render, so this is last render's value
}

Follow-up you should expect: why does this return the previous value and not the current one?

2. Rendering, Reconciliation and State

Q14

What causes a component to re-render?

Its own state changes, its props change, a context it consumes changes, or its parent re-renders. React then reconciles the result against the previous tree and touches only the DOM that actually differs.

Q15

Does a parent re-render force every child to re-render?

It causes children to render — run their function — unless they are memoized and their props are shallowly equal. It does not mean the DOM is rebuilt: reconciliation decides that separately. Conflating 're-render' with 'DOM update' is the most common mid-level mistake on this question.

Q16

What is the difference between rendering and committing?

Rendering is the pure phase where React calls your components and works out what the UI should be — it can happen more than once, and can be thrown away. Committing is where React applies the changes to the DOM and runs layout effects, and it happens once per update. Side effects during render break this contract.

Q17

What is reconciliation?

React compares the newly rendered tree with the previous one and computes the minimal set of DOM operations. It uses two heuristics: elements of different types produce different trees, and keys identify which children are the same across renders.

Q18

Why is 'Virtual DOM means React is faster' a wrong answer?

The Virtual DOM is a mechanism for computing minimal updates, not a speed guarantee. A React app with unnecessary re-renders, heavy render-time computation and new object props everywhere will be slower than plain DOM code. Say the mechanism, not the marketing.

Q19

Why are keys important, and why is an array index a risky key?

Keys tell React which item is which across renders, so it can move DOM nodes and preserve component state instead of recreating them. An index changes when the list is reordered, filtered or has items inserted, so React matches the wrong items — the visible symptom is input values or checkbox states jumping to the wrong row.

Q20

How does React decide to preserve or reset a component's state?

State is tied to the component's type and position in the tree. Keep both the same and state survives; unmount the component or change its key and the state is discarded.

Q21

When would you change a key deliberately?

To reset a subtree on purpose — for example, resetting a form when the selected record changes by putting key={recordId} on the form. It is cleaner than an effect that manually clears every field, and it is a question interviewers use to separate people who understand the model from people who memorised 'always use a unique key.'

Coding Tasks — Rendering and State

C1 · Coding

Predict the bug. Each Row holds its own input state. The user types into three rows, then deletes the first item. What happens?

{items.map((item, i) => <Row key={i} item={item} />)}

Answer: React matches components by key, so after the delete, the row that was at index 1 is now index 0 and inherits the state of the deleted row. Every typed value shifts up by one position while the item data shifts correctly — a mismatch that looks like data corruption. Fix: key={item.id}.

C2 · Coding

Refactor this reset logic.

// Before — an effect that has to remember every field
useEffect(() => {
  setName(record.name);
  setEmail(record.email);
  setPhone(record.phone);
}, [record.id]);
// After — let React reset it
<EditForm key={record.id} record={record} />

What it tests: whether you know that state is tied to type and position, and that changing a key is a legitimate tool rather than only a list requirement.

3. Performance and Profiling

Q22 · ★ Added

How do you actually find a performance problem, before optimizing anything?

Reproduce it, then measure: the Profiler to see which components render and how long the commit takes, 'highlight updates' to spot renders you did not expect, the Performance panel for long tasks and layout thrashing, and the Network panel to check whether it is a rendering problem at all. Optimizing without a measurement is the answer that fails this question.

Q23

How would you fix an app with unnecessary re-renders?

Find them first, then apply the smallest fix: memoize the component receiving unstable props, stabilise the props with useMemo/useCallback, move state down so fewer components subscribe to it, or split a context that changes too often. Then measure again.

Q24

A list of 10,000 rows is janky. What do you do?

Virtualize it so only the visible rows are in the DOM, and paginate or lazy-load the data behind it. Memoizing the row component helps only after the DOM node count is under control — the browser, not React, is the bottleneck at that size.

Q25

How does code splitting work with lazy and Suspense?

lazy plus a dynamic import moves a component into its own chunk that is fetched on demand; Suspense supplies the fallback UI while it loads. Suspense itself does not split anything — that distinction is the follow-up question.

Q26 · ★ Added

Your bundle is 3 MB. How do you diagnose and reduce it?

Run a bundle analyzer to see what is actually in it — usually one or two large dependencies (a date library, an icon set, a charting library imported wholesale). Then: route-level code splitting, tree-shakeable imports instead of whole-package imports, lighter alternatives, and moving anything that does not need to be interactive out of the client bundle.

Q27 · ★ Added

What do useTransition and useDeferredValue do?

They mark an update as non-urgent so React can keep the interface responsive: typing stays instant while an expensive filtered list updates behind it. useTransition wraps the state update you control; useDeferredValue wraps a value you receive. Neither makes the work faster — they change its priority.

Q28 · ★ Added

Does the React Compiler mean you can stop writing useMemo and useCallback?

Largely yes for new code — it auto-memoizes at build time based on the same rules you would apply by hand, and it has been stable since late 2025. The honest interview answer: it removes most manual memoization, it requires your components to actually follow the rules of React to work, and existing manual memoization is not automatically wrong.

Q29 · ★ Added

A search input feels laggy while filtering a large dataset. Walk me through it.

Confirm where the time goes first. If it is the filtering, debounce the input or defer the filtered value so the keystroke renders immediately. If it is the render, virtualize. If it is a network call per keystroke, debounce plus cancel in-flight requests. Three different fixes for three different causes.

Coding Tasks — Performance

C1 · Coding

Child is wrapped in React.memo but still re-renders on every parent render. Why?

const Child = React.memo(({ config, onSelect }) => { /* ... */ });

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <Child
      config={{ theme: "dark" }}
      onSelect={() => setCount(count + 1)}
    />
  );
}

Both props are recreated on every render, so the shallow comparison always fails. Fix:

const config = useMemo(() => ({ theme: "dark" }), []);
const onSelect = useCallback(() => setCount(c => c + 1), []);

Note the functional update — it is what allows the empty dependency array.

C2 · Coding

Implement a minimal virtualized list.

function VirtualList({ items, rowHeight = 40, height = 400 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const start = Math.floor(scrollTop / rowHeight);
  const count = Math.ceil(height / rowHeight) + 1;   // +1 for the partially visible row
  const slice = items.slice(start, start + count);

  return (
    <div
      style={{ height, overflow: "auto" }}
      onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
    >
      <div style={{ height: items.length * rowHeight, position: "relative" }}>
        {slice.map((item, i) => (
          <div
            key={item.id}
            style={{ position: "absolute", top: (start + i) * rowHeight, height: rowHeight }}
          >
            {item.name}
          </div>
        ))}
      </div>
    </div>
  );
}

What it tests: the spacer div holding the full scroll height, and absolute positioning by real index. Candidates who slice the array but forget the spacer produce a list that cannot scroll.

C3 · Coding

Typing in the search box is laggy because the filter is expensive. Fix it without debouncing.

const deferredQuery = useDeferredValue(query);
const results = useMemo(
  () => items.filter(i => i.name.includes(deferredQuery)),
  [items, deferredQuery]
);

The input stays controlled by query so keystrokes are instant, while the list catches up from the deferred value.

4. State Management and Context

Q30

How does Context work, and what performance problem does it cause?

A Provider supplies a value that any descendant can read with useContext, avoiding prop drilling. The problem: when the value changes, every consumer re-renders, even those that only use an unchanged part of it. Fixes are splitting the context by update frequency, memoizing the value object, and keeping rapidly changing state out of context entirely.

Q31

How do you share state between sibling components?

Lift it to the nearest common parent. If the tree is deep and the value is genuinely global — theme, current user, language — use Context. If it is complex application state with many writers, use a state library. The wrong answer is jumping straight to a library.

Q32

Context, Redux, or a lighter state library — how do you choose?

Context is a transport mechanism, not a state manager; it is right for stable, infrequently changing values. Redux is right when you need predictable transitions, middleware and time-travel debugging on complex global state. Lighter libraries win when you want fine-grained subscriptions without the boilerplate. Choose on update frequency and team familiarity, not on popularity.

Q33 · ★ Added

What is the difference between server state and client state?

Server state is data you do not own — it is remote, shared, can go stale, and needs caching, deduplication and refetching. Client state is UI state you fully own: modal open, current tab, form draft. Putting server data into a global client store is why so many apps end up hand-rolling a broken cache; a data-fetching library handles it properly.

Q34 · ★ Added

Where should a piece of state live?

As close as possible to where it is used, and lifted only as far as the nearest component that genuinely needs it. Most 'we need global state' problems are actually 'we put this state five levels too high' problems, and the symptom is the whole page re-rendering when one input changes.

Q35

What are the core pieces of Redux, and what does modern Redux look like?

Store, actions, reducers, dispatch, selectors and a Provider — with three principles: one source of truth, read-only state, and pure reducer updates. In practice today you write Redux Toolkit with slices and hooks; if you demonstrate connect with mapStateToProps, expect to be asked why you are not using the modern API.

Q36 · ★ Added

Why is storing derived state in state a bug?

Because you now have two sources of truth that can disagree, and you need an effect to keep them in sync — which adds a render and a class of stale-data bugs. If a value can be computed from existing state or props, compute it during render, and memoize it only if the computation is expensive.

Coding Tasks — State and Context

C1 · Coding

Every consumer re-renders when the mouse moves. Fix the provider.

// Broken — new object identity on every render, and unrelated state in one context
<AppContext.Provider value={{ user, theme, setTheme, cursor }}>

Split by update frequency and memoize each value:

const authValue  = useMemo(() => ({ user }), [user]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);

<AuthContext.Provider value={authValue}>
  <ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
</AuthContext.Provider>

High-frequency values like cursor position should not be in context at all.

C2 · Coding

Write a cart reducer with add, remove and quantity update.

function cartReducer(state, action) {
  switch (action.type) {
    case "add": {
      const existing = state.find(i => i.id === action.item.id);
      return existing
        ? state.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i)
        : [...state, { ...action.item, qty: 1 }];
    }
    case "remove":
      return state.filter(i => i.id !== action.id);
    case "setQty":
      return state.map(i =>
        i.id === action.id ? { ...i, qty: Math.max(1, action.qty) } : i
      );
    default:
      return state;
  }
}

// Total is derived during render — never stored in state
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);

What it tests: immutable updates, the add-versus-increment branch, and whether you store the total (a common mistake) or compute it.

5. Data Fetching and Async

Q37

How do you fetch data in a component?

An effect with the request, handling loading, error, success and empty states, with cleanup and a dependency array containing whatever the request depends on. In production I would use a data-fetching library rather than hand-rolling caching and cancellation.

Q38 · ★ Added

Two requests are in flight and the slower one resolves last. What happens, and how do you prevent it?

The stale response overwrites the fresh one and the UI shows the wrong data — a race condition, and one of the most common real bugs in React apps. Prevent it by aborting the previous request in the effect cleanup, or by tracking a flag or request id in the closure and ignoring responses that are no longer current.

Q39 · ★ Added

How does AbortController fit into effect cleanup?

Create a controller, pass its signal to the request, and call abort() in the cleanup function. This cancels in-flight work when dependencies change or the component unmounts, which prevents both the race condition above and the 'state update on an unmounted component' class of problems.

Q40 · ★ Added

Why would you use a data-fetching library rather than useEffect plus fetch?

Because caching, request deduplication, background refetching, retries, pagination, stale-while-revalidate and cancellation are all things you will otherwise implement badly. The interview point is knowing that these are the requirements, whether or not you name a specific library.

Q41 · ★ Added

What is an optimistic update, and what has to be true for it to be safe?

You update the UI immediately, assuming the request will succeed, and roll back if it fails. It is safe when failure is rare, rollback is straightforward, and the user is not making an irreversible decision on the optimistic state. React 19's useOptimistic gives this a first-class API.

Q42 · ★ Added

What does a complete data-loading implementation cover?

Loading, success, error, empty, and a retry path — plus cancellation and a decision about what to show on refetch (spinner versus stale data). Candidates who only handle loading and success are visibly writing tutorial code.

Q43

Which HTTP status codes do you handle differently on the frontend?

401 means re-authenticate, 403 means show a permission message rather than a login prompt, 404 means an empty state rather than an error, 409 means a conflict the user has to resolve, and 5xx means retry or show a generic failure. Treating everything as 'something went wrong' is what this question is checking for.

Q44 · ★ Added

Polling, WebSocket or server-sent events?

Polling for infrequent updates where simplicity wins. SSE for one-way server-to-client streams like notifications or progress. WebSocket for genuinely bidirectional, low-latency traffic like chat or collaborative editing. In React, the subscription belongs in an effect with cleanup, and reconnection has to be handled explicitly.

Coding Tasks — Data Fetching

C1 · Coding

Find and fix the race condition.

// Broken — switch users quickly and the slower response wins
useEffect(() => {
  fetch(`/api/users/${id}`)
    .then(r => r.json())
    .then(setUser);
}, [id]);
// Fixed
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(err => { if (err.name !== "AbortError") setError(err); });

  return () => controller.abort();
}, [id]);

The detail: swallowing AbortError specifically. Catching everything into an error state means every navigation shows a spurious error.

C2 · Coding

Write useFetch(url) with loading, error and cancellation.

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error(`Request failed: ${r.status}`);
        return r.json();
      })
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name === "AbortError") return;
        setState({ data: null, loading: false, error: err });
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

The detail: fetch does not reject on 4xx or 5xx. Checking r.ok is the line most candidates miss.

C3 · Coding

Add retry with exponential backoff and jitter.

async function retry(fn, attempts = 3, base = 500) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
      await new Promise(r => setTimeout(r, base * 2 ** i + Math.random() * 100));
    }
  }
}

Follow-up: which requests should never be retried? Anything non-idempotent without an idempotency key.

6. Forms and Controlled Components

Q45

Controlled or uncontrolled components?

Controlled means React state is the source of truth via value and onChange; uncontrolled means the DOM holds it and you read it with a ref. Controlled gives you validation, conditional logic and derived UI; uncontrolled is simpler and faster for large forms where you only need values on submit.

Q46 · ★ Added

A 40-field controlled form gets slow while typing. What is happening?

Every keystroke updates state at the form level and re-renders all 40 fields. Options: move state into each field, switch to uncontrolled inputs read on submit, or use a form library that subscribes fields individually. This is one of the most common real-world React performance issues and it almost never appears in question lists.

Q47 · ★ Added

How do you handle validation?

Validate on blur and on submit rather than on every keystroke, keep the schema in one place so the same rules can run on the client and the server, and show one clear message per field. Client validation is a user-experience feature; it is never the security boundary.

Q48 · ★ Added

What are forwardRef and useImperativeHandle, and what changed in React 19?

forwardRef let a parent pass a ref through to a child's DOM node; useImperativeHandle let the child expose a limited API like focus() instead of the raw node. In React 19 ref is an ordinary prop, so forwardRef is no longer needed for new components — it still works and is documented as heading toward deprecation.

Q49 · ★ Added

Build a search input that calls an API as the user types. What does it need?

Debounce the input, cancel the previous request, handle the empty query, guard against out-of-order responses, and keep the input itself controlled so typing never lags behind the network. Interviewers ask this because it touches effects, cleanup, closures and race conditions in one small component.

Coding Tasks — Forms

C1 · Coding

This 40-field form re-renders every field on every keystroke. Refactor it.

// Before — one state object at the form level
const [values, setValues] = useState({});
<input value={values.name} onChange={e => setValues({ ...values, name: e.target.value })} />
// After — uncontrolled, read once on submit
function Form({ onSubmit }) {
  const formRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(Object.fromEntries(new FormData(formRef.current)));
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input name="name" defaultValue={initial.name} />
      {/* 39 more, none of them re-rendering while you type */}
    </form>
  );
}

What it tests: knowing that controlled is a default, not a law, and that FormData plus name attributes is the escape hatch.

C2 · Coding

Build a 6-box OTP input.

Requirements: one digit per box, auto-advance on entry, backspace moves to the previous box when the current one is empty, pasting a 6-digit code fills every box, non-digits rejected, onComplete fires once when full.

function OtpInput({ length = 6, onComplete }) {
  const [values, setValues] = useState(() => Array(length).fill(""));
  const refs = useRef([]);

  const commit = (next) => {
    setValues(next);
    if (next.every(Boolean)) onComplete(next.join(""));
  };

  const handleChange = (i, e) => {
    const digit = e.target.value.replace(/\D/g, "").slice(-1);
    if (!digit) return;
    const next = [...values];
    next[i] = digit;
    commit(next);
    refs.current[i + 1]?.focus();
  };

  const handleKeyDown = (i, e) => {
    if (e.key === "Backspace" && !values[i]) refs.current[i - 1]?.focus();
  };

  const handlePaste = (e) => {
    const digits = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
    if (!digits) return;
    e.preventDefault();
    const next = Array(length).fill("");
    [...digits].forEach((d, i) => { next[i] = d; });
    commit(next);
    refs.current[Math.min(digits.length, length - 1)]?.focus();
  };

  return values.map((v, i) => (
    <input
      key={i}
      ref={el => { refs.current[i] = el; }}
      value={v}
      inputMode="numeric"
      maxLength={1}
      onChange={e => handleChange(i, e)}
      onKeyDown={e => handleKeyDown(i, e)}
      onPaste={handlePaste}
    />
  ));
}

Why this one comes up so often: it is small enough for 30 minutes and it exercises refs, controlled inputs, keyboard handling and paste — and most candidates forget paste entirely.

7. Errors, Bugs and Edge Cases

Q50 · ★ Added

What is an error boundary, and what does it not catch?

A component that catches render-time errors in its subtree and shows a fallback instead of unmounting the whole app. It does not catch errors in event handlers, in async code, during server rendering, or thrown by the boundary itself — those need normal try/catch. Not having one anywhere in the tree means a single render error blanks the page.

Q51 · ★ Added

You suspect a memory leak in a React app. How do you find it?

Take heap snapshots across repeated mount/unmount cycles of the suspect route and look for detached DOM nodes and growing listener counts. The usual cause is an effect that subscribes without cleaning up — a timer, an event listener, a socket, an observer. Every subscription in an effect needs a matching teardown.

Q52 · ★ Added

You get 'Cannot update a component while rendering a different component.' What did you do?

Called a state setter for another component during render instead of in an effect or an event handler. Rendering must be pure; moving the update into useEffect or into the handler that caused it resolves it.

Q53

When do you use && versus a ternary in JSX, and what is the trap?

&& for render-or-nothing, ternary for one-of-two. The trap is numbers: {count && <List/>} renders a literal 0 when the count is zero, because React renders numbers but not booleans. Write {count > 0 && ...}.

Q54 · ★ Added

When is dangerouslySetInnerHTML acceptable?

Only when the HTML comes from a source you control or has been sanitized, because React's escaping is what normally protects you from XSS. Rendering user-submitted or third-party HTML directly is the single most common self-inflicted security hole in a React app.

Q55

Why doesn't state update immediately after you call the setter?

The setter schedules an update; the variable in the current scope still holds the old value. React 18 onward batches updates automatically — including inside promises, timeouts and native handlers — and renders once with the final result. If you need the new value, use a functional update or read it in an effect.

Coding Tasks — Errors and Edge Cases

C1 · Coding

Write an error boundary with a reset path.

class ErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };            // render the fallback
  }

  componentDidCatch(error, info) {
    logToService(error, info);   // report it
  }

  render() {
    if (this.state.error) {
      return (
        <div role="alert">
          <p>Something went wrong.</p>
          <button onClick={() => this.setState({ error: null })}>Try again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

Follow-ups you should expect: why does this still have to be a class, and what does it not catch? Event handlers, async code and server rendering.

C2 · Coding

Find four bugs in this component.

function Cart({ items, onEmpty }) {
  const [total, setTotal] = useState(0);

  if (items.length === 0) onEmpty();

  useEffect(() => {
    const id = setInterval(refreshPrices, 5000);
  }, []);

  return <div>{items.length && <List items={items} />}</div>;
}

1. onEmpty() is called during render — a side effect in the pure phase, and it will throw "cannot update a component while rendering" if the parent sets state.

2. The interval is never cleared — a leak on every unmount, and duplicated intervals in Strict Mode.

3. items.length && renders a literal 0 when the cart is empty. Use items.length > 0 &&.

4. total is derived state — compute it from items during render instead of storing it.

8. Routing

Q56

What are the main router types and when would you use each?

A history-based router for normal applications with clean URLs, a hash-based router when the server cannot be configured to serve the app on every path, and a memory router for tests and non-browser environments. Modern routers also expose a data-router API where routes are declared as objects with loaders.

Q57 · ★ Added

How do you implement protected routes?

A wrapper that reads the auth state and either renders the route or redirects, with a third 'still loading' state — otherwise you flash the login page on every refresh while the session is being restored. The client-side guard is UX only; the API must enforce authorization independently.

Q58 · ★ Added

What are nested routes and layouts good for?

Rendering a shared shell — sidebar, header, tabs — once while only the inner section changes on navigation, with each level owning its own segment of the URL. It removes the duplicate layout wrapper that otherwise appears in every page component.

Q59 · ★ Added

How do you combine routing with code splitting and data loading?

Lazy-load each route so the initial bundle only contains the entry route, and start the data request as the navigation begins rather than after the component mounts. Fetching inside the lazy component's effect produces a request waterfall: chunk downloads, then component mounts, then data starts loading.

Coding Tasks — Routing

C1 · Coding

Implement a protected route.

function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <FullPageSpinner />;                     // the state most people forget
  if (!user) return <Navigate to="/login" state={{ from: location }} replace />;

  return children;
}

The details that separate answers: the third loading state (without it you flash the login page on every refresh), replace so the protected URL does not stay in history, and passing from so login can send the user back.

C2 · Coding

Write useQueryParam(key) that keeps state in the URL.

function useQueryParam(key, defaultValue = "") {
  const [params, setParams] = useSearchParams();
  const value = params.get(key) ?? defaultValue;

  const setValue = useCallback((next) => {
    setParams(prev => {
      const p = new URLSearchParams(prev);
      next ? p.set(key, next) : p.delete(key);
      return p;
    }, { replace: true });
  }, [key, setParams]);

  return [value, setValue];
}

Why it is asked: filters and search terms belong in the URL so the page is shareable and survives a refresh. replace: true stops every keystroke from creating a history entry.

9. SSR, Server Components and Hydration

Q60

CSR, SSR, SSG, ISR — what is the actual difference?

Client rendering ships an empty shell and builds the UI in the browser. Server rendering produces HTML per request. Static generation produces it at build time. Incremental regeneration is static output refreshed on a schedule or on demand. The trade-off axis is freshness against time-to-first-byte and infrastructure cost.

Q61

What are Server Components, and how do they differ from Client Components?

Server Components run only on the server, can access data directly, send no JavaScript to the browser, and cannot use state, effects or event handlers. Client Components are interactive and ship to the browser. The practical benefit is bundle size: the data-fetching and formatting code never reaches the client.

Q62

What is hydration, and what causes a hydration mismatch?

Hydration is React attaching event handlers and state to server-rendered HTML. Mismatches come from anything that renders differently on server and client: Date.now(), Math.random(), locale or timezone formatting, reading window, localStorage or cookies during render, and browser extensions modifying the DOM. Move that logic into an effect or render it only after mount.

Q63 · ★ Added

What is useId for?

Generating ids that are stable between server and client, for things like linking a label to an input. Hand-rolled counters or random ids produce different values on each side and cause hydration mismatches — that is the whole reason the hook exists.

Q64 · ★ Added

What did React 19 change that affects how you write forms and async UI?

Actions let a function be passed directly to a form and handle the pending state for you, useActionState tracks the result and pending status, useFormStatus exposes it to nested components, useOptimistic handles optimistic UI, and use reads a promise or context during render. Together they replace a lot of manual isSubmitting state.

Coding Tasks — SSR and Hydration

C1 · Coding

This throws a hydration mismatch. Fix it.

// Broken — server and client render different text
function Clock() {
  return <span>{new Date().toLocaleTimeString()}</span>;
}
// Fixed — render nothing time-dependent until after mount
function Clock() {
  const [time, setTime] = useState(null);
  useEffect(() => {
    const tick = () => setTime(new Date().toLocaleTimeString());
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
  return <span>{time ?? "--:--:--"}</span>;
}

The rule: anything that differs between server and client — time, random values, window, localStorage, timezone, user agent — must be read in an effect, not during render.

C2 · Coding

Write the client-only gate used for widgets that cannot render on the server.

function useMounted() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  return mounted;
}

// Usage
const mounted = useMounted();
if (!mounted) return <Skeleton />;
return <MapWidget />;

Follow-up: what does this cost? An extra render and no server-rendered content for that subtree — so use it for genuinely browser-only widgets, not as a blanket fix for mismatches you have not diagnosed.

10. Testing

Q65 · ★ Added

What do you actually test in a React application?

Behaviour a user can observe: rendering the right thing for given props and state, what happens on interaction, and the loading, error and empty paths. Unit-test pure logic and custom hooks, integration-test the components that compose them, and keep a thin end-to-end layer over the critical flows.

Q66 · ★ Added

How do you test a custom Hook?

Through a component that uses it, or with a hook-testing utility, asserting on the values it returns after actions rather than on its internals. If a hook is hard to test, it is usually doing two things and should be split.

Q67 · ★ Added

How do you handle API calls in tests?

Intercept at the network layer rather than mocking your own fetch wrapper, so the test exercises the real request-handling code. Then assert on what the user sees for success, error and empty responses. Mocking the module under test is how you get a green suite and a broken feature.

Q68 · ★ Added

Why is testing implementation details a problem?

Tests that assert on state variables, internal function calls or component structure break on every refactor while catching no real bugs, so the team starts deleting them. Assert on what renders and what happens when the user interacts — that survives refactoring and actually fails when something is broken.

Coding Tasks — Testing

C1 · Coding

Test a component that fetches and renders a user.

test("shows a loading state and then the user", async () => {
  render(<UserCard id="1" />);

  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  expect(await screen.findByText("Priya Sharma")).toBeInTheDocument();
});

test("shows an error message when the request fails", async () => {
  server.use(failUserRequest());          // network-level intercept, not a fetch mock
  render(<UserCard id="1" />);

  expect(await screen.findByRole("alert")).toHaveTextContent(/could not load/i);
});

The details: findBy* for anything asynchronous (getBy* throws immediately), querying by role and text rather than by test id where possible, and covering the error path — a suite that only tests the happy path is what interviewers are probing for.

C2 · Coding

Test a custom Hook.

test("useCounter increments", () => {
  const { result } = renderHook(() => useCounter(0));

  act(() => result.current.increment());

  expect(result.current.count).toBe(1);
});

Follow-up: why is act needed? Because the state update has to be flushed before you assert, otherwise you read the value from before the render.

11. TypeScript and Code Quality

Q69 · ★ Added

How do you type props, including children?

An explicit interface or type for the props, with React.ReactNode for children and the appropriate event type for handlers. Avoid any and avoid over-generic props objects — the value of typing props is that the compiler catches wrong usage at the call site, and both of those give that away.

Q70 · ★ Added

How do you type what a custom Hook returns?

Return an object with named fields rather than a tuple once there are more than two values, so consumers cannot mix up the order. For a tuple, use as const or an explicit tuple type, otherwise TypeScript widens it to an array union and destructuring loses the types.

Q71 · ★ Added

Which lint rules genuinely matter in a React codebase?

The exhaustive-deps rule for hooks, the rules-of-hooks rule, and a no-unused rule that is actually enforced in CI. A rule that everyone disables inline is worse than no rule, because it trains the team to add the suppression comment without reading it.

Coding Tasks — TypeScript

C1 · Coding

Type a generic list component.

type ListProps<T> = {
  items: T[];
  keyOf: (item: T) => string;
  renderItem: (item: T) => React.ReactNode;
};

function List<T>({ items, keyOf, renderItem }: ListProps<T>) {
  return <ul>{items.map(item => <li key={keyOf(item)}>{renderItem(item)}</li>)}</ul>;
}

// Usage — T is inferred, renderItem's parameter is fully typed
<List items={users} keyOf={u => u.id} renderItem={u => u.name} />

What it tests: whether you reach for a generic or fall back to any[]. The payoff is that renderItem gets the real item type with no annotation at the call site.

C2 · Coding

Fix the return type of this Hook.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn(o => !o), []);
  return [on, toggle];        // inferred as (boolean | (() => void))[]
}
  return [on, toggle] as const;   // readonly [boolean, () => void]

Without as const, destructuring gives you a union type on both positions and the consumer has to cast. Beyond two values, return a named object instead so callers cannot swap the order.

12. Architecture and Scenario Questions

Q72

How would you structure a medium-sized React application?

By feature rather than by file type, so everything one feature needs sits together, with a shared layer for genuinely cross-cutting components, hooks and services. Structuring by type — all components in one folder, all hooks in another — stops scaling somewhere around thirty components, and interviewers know it.

Q73 · ★ Added

A page takes six seconds to load. Walk me through your diagnosis.

Network first: is it one slow API call, a waterfall of sequential calls, or an oversized bundle? Then rendering: a long commit, an unvirtualized list, or expensive work on every render? Then the data itself: are you fetching far more than you display? Name the order — that sequencing is what the question is testing, not the fixes.

Q74 · ★ Added

A junior's pull request wraps every value in useMemo and every function in useCallback. What do you say in review?

That memoization has a cost and only pays off where there is a measured problem, and that with the React Compiler most of it is now redundant anyway. Then give one concrete example from the diff where it helps and one where it does not — a review that just says 'remove these' teaches nothing.

Q75 · ★ Added

How would you migrate a class-component codebase to hooks?

Not all at once. Both styles interoperate, so convert opportunistically — new components as functions, existing ones when you are already changing them — and start with leaf components rather than containers holding lifecycle-heavy logic. Error boundaries still need classes unless you adopt a library wrapper.

Q76 · ★ Added

When do you build a shared component instead of copying it?

On the third occurrence, and only if the variations are configuration rather than genuinely different behaviour. A shared component with eleven boolean props is worse than three separate components — premature abstraction costs more than duplication in UI code.

Q77 · ★ Added

How do you handle authentication token refresh in a React app?

A single interceptor at the HTTP layer that catches a 401, refreshes once, queues the concurrent requests that failed during the refresh, and replays them — with a hard logout if the refresh itself fails. Storage choice matters too: any token in localStorage is readable by any script on the page.

Machine Coding Rounds — Build These End to End

These are 45 to 90 minute rounds. The code matters less than the first five minutes: clarify requirements, state the data shape, name the components, then type. Candidates who open with JSX usually lose here even when the feature works.

M1 · Machine Coding (45–90 min)

Autocomplete / typeahead.

Requirements: Fetch suggestions as the user types, debounce the input, cancel in-flight requests, keyboard navigation with arrow keys and Enter, close on Escape and on outside click, highlight the matched substring, show loading and no-results states, and make it accessible with role='combobox' and aria-activedescendant.

Evaluation: The race condition and the keyboard support. Almost everyone gets the fetch working; roughly half handle out-of-order responses, and fewer handle the keyboard.

M2 · Machine Coding (45–90 min)

Infinite scroll list.

Requirements: Load a page when the sentinel enters the viewport using IntersectionObserver, show a loading row, stop cleanly when there are no more pages, handle a failed page load with a retry, and do not fire duplicate requests while one is pending.

Evaluation: Disconnecting the observer in cleanup, and the in-flight guard. The classic bug is firing three requests for the same page because the sentinel stays visible while the first is still loading.

M3 · Machine Coding (45–90 min)

Nested comment thread.

Requirements: Render arbitrarily deep replies recursively, collapse and expand a subtree, add a reply at any level with immutable state updates, and keep it performant when one thread has hundreds of nodes.

Evaluation: Whether you flatten the tree into a map keyed by id or recurse over a nested structure, and whether the add-reply update mutates. Say which shape you picked and why before you write it.

How to Work Through This

Cover the unstarred questions first if this is your first pass — they are the foundation and they come up in every loop. The starred ones are where a 2–3 year candidate is separated from a 1 year candidate, because they require having debugged something rather than having read something.

Prepare three stories before the interview: a performance problem you diagnosed and fixed, a bug caused by a stale closure or a race condition, and a technical decision you would make differently now. Most senior-facing questions in this list can be answered with one of those three, and a real example beats a correct definition every time. Build that muscle with timed mock interviews at Roundexa.com.

Ready to Practice?

Take a free AI mock interview on Roundexa and get instant, actionable feedback before the real one.

Practice on Roundexa