Skip to content
React interview 6 min read

Advanced React Interview Questions

Advanced React interviews move past “what is a hook” and probe how the library actually works: how reconciliation diffs trees, what truly triggers a re-render, when memoization pays off, and how the concurrent renderer in React 18/19 changes scheduling. These questions separate people who can wire up a form from people who can reason about performance and correctness under load. The set below covers reconciliation and keys, re-render causes, memoization, concurrent features, context performance, error boundaries, SSR/hydration, and composition patterns.

Reconciliation and keys

How does React’s reconciliation algorithm work?

When state changes, React renders a new element tree and diffs it against the previous one. It compares element types at each position: if the type is the same, React reuses the DOM node and updates props; if the type differs, it unmounts the old subtree and mounts a new one. This makes the diff roughly O(n) instead of the O(n³) a general tree-diff would cost.

// Same type -> DOM node reused, only the className changes
<div className="a" /> // becomes
<div className="b" />

// Different type -> old <div> torn down, new <span> mounted fresh
<div /> // becomes
<span />

Why do keys matter and what breaks when you use the array index?

Keys tell React which list items correspond across renders. Using the array index as a key works only for static lists; for lists that reorder, insert, or delete, the index stays attached to a position rather than to an item, so React reuses the wrong component instance and local state leaks between rows.

// Buggy: each input's typed value follows the index, not the item
{items.map((item, i) => <Row key={i} item={item} />)}

// Correct: a stable identity per item
{items.map((item) => <Row key={item.id} item={item} />)}

Reordering a list keyed by index can silently move one row’s input value onto another row. Always use a stable, unique id.

What causes re-renders

What actually triggers a component to re-render?

A component re-renders when its own state changes (useState/useReducer), when its parent re-renders, or when a context it consumes changes. Notably, props changing is not an independent trigger — props change because a parent re-rendered. A parent re-render re-renders all children by default, even children whose props are unchanged.

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <Child /> {/* re-renders on every click, even with no props */}
    </>
  );
}

How does React.memo stop that?

React.memo wraps a component so it skips re-rendering when its props are shallowly equal to the previous render. It does nothing about state or context changes inside the component — only props.

const Child = React.memo(function Child({ label }) {
  console.log("render");
  return <span>{label}</span>;
});

Memoization: useMemo, useCallback, useMemo for props

When should you use useMemo and useCallback?

useMemo caches a computed value; useCallback caches a function identity. They matter mainly to keep referential equality stable so that React.memo children or hook dependency arrays don’t see “new” values every render. They are not free — they cost memory and a dependency comparison — so applying them everywhere is an anti-pattern.

const sorted = useMemo(() => rows.sort(byName), [rows]);
const onSelect = useCallback((id) => setSelected(id), []);
ToolCachesPrimary use
useMemoa valueexpensive computation, stable object/array props
useCallbacka functionstable handler passed to a memoized child
React.memoa renderskip re-render on equal props

Concurrent features

What problem do useTransition and useDeferredValue solve?

React 18 introduced concurrent rendering, which lets React interrupt and reprioritize work. useTransition marks a state update as non-urgent so urgent updates (like typing) stay responsive while the heavy update renders in the background. useDeferredValue does the reverse end: it gives you a lagging copy of a value to render the expensive part from.

function Search() {
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState([]);

  function onChange(e) {
    setQuery(e.target.value); // urgent
    startTransition(() => setResults(filter(e.target.value))); // deferred
  }
  return (
    <>
      <input value={query} onChange={onChange} />
      {isPending && <Spinner />}
      <List items={results} />
    </>
  );
}

What is automatic batching in React 18?

Before 18, React batched state updates only inside event handlers. React 18 batches updates everywhere — including promises, timeouts, and native handlers — so multiple setState calls in an async callback now trigger a single re-render.

Output:

// React 18: one render after both updates
render

Context performance

Why can Context cause performance problems?

Every consumer of a context re-renders whenever the context value changes by reference — even if the part of the value it reads is unchanged. Passing an inline object as value recreates that reference every render, forcing all consumers to re-render.

// Bad: new object identity every render
<Ctx.Provider value={{ user, theme }}>

// Better: memoize the value
const value = useMemo(() => ({ user, theme }), [user, theme]);
<Ctx.Provider value={value}>

For frequently changing slices, split into multiple contexts or use an external store (Zustand, Redux) with selector-based subscriptions so components subscribe only to what they read.

Error boundaries

How do you catch render errors, and what can’t they catch?

Error boundaries catch errors thrown during rendering, in lifecycle methods, and in constructors of the subtree below them. They are still class components because the catching lifecycle (getDerivedStateFromError/componentDidCatch) has no hook equivalent. They do not catch errors in event handlers, async code, or SSR — handle those with try/catch.

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  componentDidCatch(error, info) {
    logError(error, info);
  }
  render() {
    return this.state.hasError ? <Fallback /> : this.props.children;
  }
}

SSR and hydration

What is hydration and what causes a hydration mismatch?

Server-side rendering ships HTML, then the client “hydrates” it by attaching event listeners and reconciling the existing DOM with React’s tree. A mismatch occurs when the server-rendered markup differs from the first client render — common causes are Date.now(), random values, or reading window/localStorage during render. React warns and, in 18, may discard the server HTML and re-render that subtree on the client.

// Mismatch: server and client compute different text
function Clock() {
  return <span>{new Date().toLocaleTimeString()}</span>;
}

// Fix: render the dynamic part after mount
function Clock() {
  const [time, setTime] = useState(null);
  useEffect(() => setTime(new Date().toLocaleTimeString()), []);
  return <span>{time ?? "--:--:--"}</span>;
}

What do React Server Components change?

Server Components (stable in the React 19 / Next.js App Router model) render on the server and never ship their code or dependencies to the client, reducing bundle size. They can be async and fetch data directly, but cannot use state or effects. Client Components, marked with "use client", handle interactivity.

Patterns

What are render props, HOCs, and custom hooks — and which to prefer?

All three share logic. Higher-order components wrap a component and inject props; render props pass a function as children; custom hooks extract stateful logic into a reusable function. Modern React strongly favors custom hooks because they avoid wrapper nesting (“wrapper hell”) and keep the JSX tree flat.

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

Best Practices

  • Key lists by a stable id, never by array index when the list can reorder.
  • Reach for useMemo/useCallback only to fix a measured re-render or dependency-stability problem, not by reflex.
  • Memoize context value objects and split contexts by update frequency.
  • Use useTransition to keep input responsive during expensive list updates.
  • Keep render functions pure — no Date.now(), randomness, or DOM reads — to avoid hydration mismatches.
  • Prefer custom hooks over HOCs and render props for sharing stateful logic.
  • Wrap risky subtrees in error boundaries, and handle async/event errors with explicit try/catch.
Last updated June 14, 2026
Was this helpful?