React Interview Prep

React Interview Questions and Answers (2026)

React interviews tend to probe whether you understand what's happening underneath the hooks API — reconciliation, dependency arrays, and closures — not just whether you can write a component. Here are the React questions that come up most often.

  1. What is the virtual DOM and how does reconciliation work?

    The virtual DOM is a lightweight JS object tree that mirrors the real DOM. When state changes, React builds a new virtual DOM tree and diffs it against the previous one (reconciliation) rather than touching the real DOM directly, then applies only the minimal set of real DOM mutations needed. React's diffing is heuristic-based, not a generic tree-diff algorithm: it compares elements of the same type at the same position, assumes different component types produce entirely different subtrees (unmount/remount rather than diff), and uses the key prop to match list items across renders. This makes diffing O(n) instead of the theoretical O(n3) for general tree diffing, at the cost of occasionally doing more work than strictly necessary (e.g., unnecessary unmounts when a type changes).

  2. What are the Rules of Hooks and why do they exist?

    Two rules: (1) only call hooks at the top level — never inside loops, conditions, or nested functions; (2) only call hooks from React function components or custom hooks, not regular JS functions. These exist because React tracks hooks by call order, not by name — internally it's a linked list matched to a fiber node on each render. If a hook call is conditionally skipped, every subsequent hook's position shifts, and React reads stale state into the wrong slot.

    // Wrong
    if (isLoggedIn) {
      const [name, setName] = useState('');
    }
    
    // Right
    const [name, setName] = useState('');
    if (isLoggedIn) { /* use name */ }

    The eslint-plugin-react-hooks rules-of-hooks rule catches these violations at lint time.

  3. How do `useEffect` dependency arrays and cleanup functions work?

    The dependency array tells React when to re-run the effect: omitted means every render, [] means once after mount, [a, b] means whenever a or b changes by reference (Object.is comparison). The effect can return a cleanup function, which React runs before the effect re-runs and again on unmount — used for unsubscribing listeners, clearing timers, or aborting fetches.

    useEffect(() => {
      const id = setInterval(() => setCount(c => c + 1), 1000);
      return () => clearInterval(id); // cleanup
    }, []);

    A common bug is omitting a dependency that's actually used inside the effect (stale closures) — exhaustive-deps lint rule catches this. Objects/functions recreated every render as dependencies cause effects to re-run every time unless memoized with useMemo/useCallback.

  4. When should you use `useRef` instead of `useState`?

    useState triggers a re-render whenever the value changes and is for data that affects what's rendered. useRef returns a mutable .current container that persists across renders without triggering a re-render when mutated — used for DOM node references, storing previous values, timer IDs, or any "instance variable" that the render output doesn't depend on.

    function Timer() {
      const intervalRef = useRef(null);
      const [seconds, setSeconds] = useState(0);
    
      useEffect(() => {
        intervalRef.current = setInterval(() => setSeconds(s => s + 1), 1000);
        return () => clearInterval(intervalRef.current);
      }, []);
    }

    Using useState for something like an interval ID would work but cause pointless re-renders; using useRef for something the UI must reflect would cause the UI to silently go stale, since ref updates don't schedule a render.

  5. Controlled vs uncontrolled components — what's the tradeoff?

    A controlled component's value is driven entirely by React state — the input's value prop comes from state and every change goes through onChange to update it, making React the single source of truth. An uncontrolled component keeps its own internal DOM state, and React reads the current value on demand via a ref.

    // Controlled
    <input value={name} onChange={e => setName(e.target.value)} />
    
    // Uncontrolled
    <input ref={inputRef} defaultValue="Rex" />
    // later: inputRef.current.value

    Controlled components make validation, conditional disabling, and syncing multiple fields trivial, but re-render on every keystroke. Uncontrolled components perform better for large, simple forms and integrate more easily with non-React code or file inputs, but are harder to validate reactively.

  6. When do `React.memo`, `useMemo`, and `useCallback` actually help — and when are they wasted effort?

    React.memo wraps a component and skips re-rendering it if its props are shallowly equal to the last render. useMemo memoizes a computed value across renders; useCallback memoizes a function reference. They only help when: (a) the wrapped computation/render is genuinely expensive, or (b) referential stability matters — e.g., passing a callback to a memo-wrapped child or as a dependency in another hook's array. Wrapping everything "for performance" is counterproductive — each memoization has its own comparison/storage overhead, and for cheap components it's often slower than just re-rendering.

    const sorted = useMemo(() => expensiveSort(list), [list]);
    const handleClick = useCallback(() => onSelect(id), [id]);

    Profile first; don't memoize by default.

  7. Why does React warn about missing `key` props in lists, and why is index-as-key risky?

    key gives React a stable identity for each list item across renders so it can match old elements to new ones during reconciliation, instead of assuming position-based correspondence. Without keys, React falls back to index, which breaks when items are reordered, inserted, or removed in the middle — React ends up mutating the wrong DOM nodes' state (uncontrolled input values, component-local state) because it thinks "item at index 2" is the same logical item even though its identity changed.

    {items.map(item => (
      <Row key={item.id} data={item} /> // stable, not index
    ))}

    Index keys are acceptable only for static lists that never reorder, filter, or have items inserted/removed. Using a stable unique ID (item.id) avoids subtle bugs like stale form state following the wrong row after a deletion.

  8. Context API vs prop drilling — when is Context the right tool?

    Prop drilling means passing a prop through several intermediate components that don't use it themselves, just to reach a deeply nested consumer. Context lets a provider expose a value that any descendant can read via useContext without it being threaded through every level, which is ideal for cross-cutting concerns like theme, auth user, or locale.

    const ThemeContext = createContext('light');
    <ThemeContext.Provider value="dark">
      <App />
    </ThemeContext.Provider>
    // deep child:
    const theme = useContext(ThemeContext);

    Context isn't a general state-management replacement: any update to the provider's value re-renders every consumer, regardless of which slice they use, so it's a poor fit for high-frequency updates or large state trees — that's where Redux/Zustand with selective subscriptions do better.

  9. What are custom hooks and what problem do they solve?

    A custom hook is a JS function whose name starts with use that calls other hooks internally, letting you extract and reuse stateful logic across components without duplicating code or resorting to render props/HOC wrapper hell. Unlike a regular utility function, a custom hook can hold its own state and effects, and each component that calls it gets an independent instance of that state.

    function useFetch(url) {
      const [data, setData] = useState(null);
      useEffect(() => {
        let active = true;
        fetch(url).then(r => r.json()).then(d => active && setData(d));
        return () => { active = false; };
      }, [url]);
      return data;
    }

    This is the primary composition mechanism in modern React — logic like form handling, data fetching, or media queries gets packaged once and reused declaratively.

  10. What are error boundaries and what can't they catch?

    An error boundary is a class component implementing static getDerivedStateFromError and/or componentDidCatch that catches JS errors thrown during rendering in its child tree, logs them, and displays a fallback UI instead of unmounting the whole app. There is no hook equivalent yet — error boundaries must be classes.

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

    They do not catch errors in event handlers (those need regular try/catch), async code (setTimeout, promises), server-side rendering errors, or errors thrown in the boundary itself. This narrow scope is intentional — rendering errors leave the UI in an inconsistent state, which is the specific failure mode boundaries are designed for.

  11. At a high level, what are React Server Components and how do they differ from SSR?

    Server Components (RSC) render entirely on the server and never ship their JS to the client — no hydration cost, no bundle size impact, and they can directly access backend resources (databases, filesystem) without an API layer. This is different from traditional SSR, where the server renders HTML for the *initial* load but the same component code is also shipped to and re-executed on the client (hydration) to attach interactivity. RSCs are non-interactive by nature (no useState/useEffect) and are composed with Client Components (marked "use client") for anything needing interactivity or browser APIs.

    // Server Component (default in Next.js App Router)
    async function Posts() {
      const posts = await db.query('SELECT * FROM posts');
      return <PostList posts={posts} />;
    }

    The tradeoff is added architectural complexity (server/client boundary, serialization rules for props) in exchange for smaller client bundles and simpler data fetching.

  12. What does "lifting state up" mean and when should you do it?

    When two or more sibling components need to share or stay in sync with the same piece of state, that state should live in their closest common ancestor rather than being duplicated locally in each child. The parent then passes the state down as props and passes update functions down for children to call, making the parent the single source of truth for that data.

    function Parent() {
      const [value, setValue] = useState('');
      return (
        <>
          <Input value={value} onChange={setValue} />
          <Preview value={value} />
        </>
      );
    }

    The tradeoff is that lifting state too far up causes unrelated components to re-render together and re-couples components that were otherwise independent — the rule of thumb is to lift state to the lowest common ancestor that actually needs it, no higher.

Practicing for a real interview? Get live AI help with your answers, in the browser, free to start.

Start a free session