Skip to main content
PProfileMatch

React Interview Questions and Answers

27 questions8 beginner · 13 intermediate · 6 advancedLast reviewed

This set is for front-end and full-stack candidates preparing for React rounds, from a first junior screen to a senior discussion about performance and architecture. It focuses on the reasoning interviewers want to hear: why React behaves the way it does, not only which hook to reach for.

Practise by explaining each answer aloud in under a minute before you open it, then compare what you missed. For the code samples, type them into a scratch project and change one thing, such as removing a key or a dependency, and predict what breaks. If JavaScript fundamentals feel shaky, revisit the React course and JavaScript fundamentals alongside the JavaScript question set.

  1. 1What is a component in React, and how are props different from state?Beginner

    A component is a function that receives props and returns a description of UI (JSX). React calls it to find out what to show, and you build screens by composing small components into bigger ones.

    Props are inputs passed down by the parent, and the component treats them as read-only. State is memory the component owns between renders, created with useState; changing it schedules a re-render. In practice data flows down through props and events flow up through callback props.

    For example, a parcel card receives a parcel prop but owns an expanded state. A common mistake is copying a prop into state, which then goes stale when the parent sends a new value.

    What the interviewer is checking: That you can explain the data-flow model in your own words, including the stale copy pitfall.

    Likely follow-up: A child component needs to change something the parent owns. How does it do that?

  2. 2Why does React need a key on list items, and why is the array index a risky choice?Beginner

    Keys let React match items between renders, so it knows which row is which. With stable keys, React reuses the same DOM node and component state for the same logical item and simply moves it when the order changes.

    A good key is unique among siblings and stable, typically the database ID. The index is only safe for a static list that never reorders, inserts or deletes. Otherwise state and DOM details such as typed text or focus stay attached to the position instead of the item, so after removing the first row the second row may show the first row's input text.

    Random keys are worse: a new key every render means every row remounts, losing state and doing extra work.

    tsx
    type Slot = { id: string; label: string };
    
    function SlotList({ slots }: { slots: Slot[] }) {
      return (
        <ul>
          {slots.map((slot, index) => (
            // Risky: when a slot is inserted or removed, index n now points at a different slot,
            // so React keeps the old row's DOM and input text for the wrong slot.
            <li key={index}>
              <label>
                {slot.label} <input defaultValue="" />
              </label>
            </li>
          ))}
        </ul>
      );
    }
    
    function SlotListFixed({ slots }: { slots: Slot[] }) {
      return (
        <ul>
          {slots.map((slot) => (
            <li key={slot.id}>
              <label>
                {slot.label} <input defaultValue="" />
              </label>
            </li>
          ))}
        </ul>
      );
    }
    
    export { SlotList, SlotListFixed };

    What the interviewer is checking: Whether you understand keys as identity for reconciliation, not just a warning to silence.

    Likely follow-up: Is it acceptable to use the index as a key for a list that is paginated but never reordered? What would you check?

  3. 3What is the difference between controlled and uncontrolled inputs? When would you pick each?Beginner

    A controlled input gets its value from React state and updates it through onChange, so React is the single source of truth. An uncontrolled input keeps its value in the DOM; you give it a defaultValue and read the value through a ref or FormData when you need it.

    Controlled inputs suit live validation, formatting as you type, disabling a button based on the value, or fields that depend on one another. Uncontrolled inputs are simpler and re-render less for plain forms that you only read on submit, and they are needed for file inputs.

    A classic bug is switching an input from uncontrolled to controlled, for example value={undefined} first and a string later. Always initialise controlled values, usually to an empty string.

    tsx
    import { useRef, useState } from "react";
    
    // Controlled: React state is the single source of truth for the value.
    function PromoCodeControlled() {
      const [code, setCode] = useState("");
      return <input value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} />;
    }
    
    // Uncontrolled: the DOM keeps the value; we read it when we need it.
    function PromoCodeUncontrolled({ onApply }: { onApply: (code: string) => void }) {
      const inputRef = useRef<HTMLInputElement>(null);
      return (
        <form
          onSubmit={(e) => {
            e.preventDefault();
            onApply(inputRef.current?.value ?? "");
          }}
        >
          <input ref={inputRef} defaultValue="" />
          <button type="submit">Apply</button>
        </form>
      );
    }
    
    export { PromoCodeControlled, PromoCodeUncontrolled };

    What the interviewer is checking: Practical judgement about forms, and awareness of the controlled/uncontrolled switching warning.

    Likely follow-up: How would you handle a large form with fifty fields without re-rendering everything on each keystroke?

  4. 4Why can't you modify state directly, for example with items.push(x)? What should you do instead?Beginner

    React finds out that something changed when you call the setter, and it compares the new value with the old one by reference. If you mutate the array in place and pass the same reference, React sees no change and may skip the re-render. Memoised children and effects that compare by reference would not notice either.

    Create a new value instead: [...items, x], items.filter(...), items.map(...), and for objects { ...user, name }. For deeply nested state, flatten it or use a helper like Immer.

    Related gotcha: state is a snapshot for each render. Calling setCount(count + 1) three times in one handler adds only one, because count is the same value each time. Use the updater form, setCount((c) => c + 1), when the next value depends on the previous one.

    What the interviewer is checking: Whether you understand reference comparison and that state is a per-render snapshot.

    Likely follow-up: How would you update one field of one item inside an array of objects held in state?

  5. 5What is useEffect for, and what should you not use it for?Beginner

    useEffect lets a component synchronise with something outside React after rendering: subscriptions, timers, network requests, browser APIs, or a third-party widget. It runs after the screen has been updated.

    It is often misused. Do not use it to compute values from props or state, because you can calculate those during render. Do not use it to react to a click, because that belongs in the event handler. And do not use it to reset state when a prop changes; give the component a different key instead.

    A useful rule of thumb: if there is no external system involved, you probably do not need an effect. Extra effects add extra render passes and make the data flow harder to follow.

    What the interviewer is checking: Whether you see effects as a synchronisation escape hatch rather than a general-purpose lifecycle callback.

    Likely follow-up: A filtered list is computed in an effect and stored in state. How would you simplify that?

  6. 6What is the virtual DOM, and why does React use it?Beginner

    The virtual DOM is the tree of lightweight JavaScript objects (React elements) that your components return. When state changes, React calls the components again to get a new tree, compares it with the previous one, and applies only the differences to the real DOM.

    The point is not that it beats hand-written DOM code in raw speed. The benefit is the declarative model: you describe what the UI should look like for the current state, and React works out the minimal updates.

    It also means work happens in two phases. Render is a pure calculation of the next tree; commit is where the DOM is updated and effects run. Performance tuning is about avoiding renders that are unnecessary or expensive, since calling your component functions still costs time even if the DOM does not change.

    What the interviewer is checking: Whether you avoid the "it is faster than the DOM" cliché and describe render versus commit.

    Likely follow-up: Does a re-render always change the DOM?

  7. 7Two sibling components need the same piece of data. How do you share it?Beginner

    Lift the state up to their closest common parent. The parent owns the value with useState and passes it down as props, along with a callback so either child can request a change. Both children then always agree because there is only one copy.

    In the sample, the seat map and the summary both show the chosen seat, and the booking component owns it. If the state is needed far down a deep tree, passing props through many layers gets noisy, and context or a store may be better. But lifting state is the first tool, and it is often enough.

    Avoid keeping a copy in each child and syncing them with an effect. That creates two sources of truth and bugs that depend on timing.

    tsx
    import { useState } from "react";
    import { renderToStaticMarkup } from "react-dom/server";
    
    function SeatMap({ selected, onSelect }: { selected: string; onSelect: (seat: string) => void }) {
      return (
        <div>
          {["A1", "B2", "C3"].map((seat) => (
            <button key={seat} aria-pressed={seat === selected} onClick={() => onSelect(seat)}>
              {seat}
            </button>
          ))}
        </div>
      );
    }
    
    function Summary({ seat }: { seat: string }) {
      return <p>You picked seat {seat}</p>;
    }
    
    // The parent owns the state; both children receive it through props.
    function Booking() {
      const [seat, setSeat] = useState("B2");
      return (
        <>
          <SeatMap selected={seat} onSelect={setSeat} />
          <Summary seat={seat} />
        </>
      );
    }
    
    console.log(renderToStaticMarkup(<Booking />));

    Output

    <div><button aria-pressed="false">A1</button><button aria-pressed="true">B2</button><button aria-pressed="false">C3</button></div><p>You picked seat B2</p>

    What the interviewer is checking: Whether you default to the simplest data-flow solution and can name its limit.

    Likely follow-up: The state is needed ten levels down. What are your options besides passing props through every level?

  8. 8When does a React component re-render?Beginner

    A component re-renders when its own state changes, when its parent re-renders, or when a context it reads changes. Notice that changing props is not the trigger by itself. By default, a parent re-render re-renders all its children, even if their props are identical.

    Re-rendering means React calls the function again. It does not necessarily change the DOM; React compares the output and only touches what differs. Setting state to the same value (by Object.is) usually lets React bail out.

    React.memo lets a child skip re-rendering when its props are shallow-equal. But re-rendering is normally cheap, so measure before optimising. The most effective fix is often to move state down so fewer components are affected.

    What the interviewer is checking: An accurate model of the render triggers, including the common belief that unchanged props prevent re-rendering.

    Likely follow-up: Typing in a search box re-renders a heavy table next to it. How do you stop that?

  9. 9How does the useEffect dependency array work, and what is the cleanup function for?Intermediate

    The effect runs after the first render and again after any render in which a value in the array changed, compared with Object.is. An empty array means once on mount. Omitting the array means after every render. List everything from the component scope that the effect reads; the exhaustive-deps lint rule checks this, and silencing it invites stale values.

    The function you return is the cleanup. React runs it before the next run of the effect and when the component unmounts, so use it to undo what the effect did: unsubscribe, clear a timer, abort a request.

    If a dependency is an object or function created during render, the effect re-runs every time. Move it inside the effect, derive a primitive, or stabilise it, rather than removing it from the array.

    What the interviewer is checking: Whether you know the exact re-run rules and treat the lint rule as correct instead of an annoyance.

    Likely follow-up: An effect re-runs on every render because it depends on an options object. What are your choices?

  10. 10A setInterval inside useEffect keeps showing the initial count. What is going on and how do you fix it?Intermediate

    This is a stale closure. Each render creates a new function that captures that render's count. With an empty dependency array the effect runs once, so the interval callback keeps the closure from the first render, where count is always 0.

    There are three fixes. Use the updater form, setCount((c) => c + 1), which does not need the current value from the closure. Add count to the dependencies so the interval is recreated on each change, which works but restarts the timer. Or keep the latest callback in a ref that the interval reads.

    The updater form is usually best. The key idea is that every render has its own values, and a function only sees the ones from the render that created it.

    tsx
    import { useEffect, useState } from "react";
    
    function MeterBuggy() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1); // count is the value from the first render: always 0
        }, 1000);
        return () => clearInterval(id);
      }, []); // the effect never re-runs, so the closure is never refreshed
      return <p>{count}</p>;
    }
    
    function MeterFixed() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        const id = setInterval(() => {
          setCount((c) => c + 1); // the updater receives the latest value
        }, 1000);
        return () => clearInterval(id);
      }, []);
      return <p>{count}</p>;
    }
    
    export { MeterBuggy, MeterFixed };

    What the interviewer is checking: Whether you understand closures per render, not just the memorised fix.

    Likely follow-up: How would you use a ref so a timer always calls the latest version of a callback prop?

  11. 11What do useMemo, useCallback and React.memo do, and how do they work together?Intermediate

    React.memo wraps a component so it skips re-rendering when its props are shallow-equal to the previous ones. useMemo caches a computed value between renders until its dependencies change. useCallback caches a function's identity, and is equivalent to useMemo(() => fn, deps).

    They combine because React.memo compares props by reference. If the parent passes a new function or object on every render, the memo check always fails. useCallback and useMemo keep those references stable so the child can really skip.

    Treat them as performance hints, not guarantees that React will never recompute. In the sample, rows are memoised, the click handler has a stable identity, and filtering only reruns when the list or query changes.

    tsx
    import { memo, useCallback, useMemo, useState } from "react";
    
    type Fare = { id: string; label: string; price: number };
    declare function track(id: string): void;
    
    const FareRow = memo(function FareRow({ fare, onSelect }: { fare: Fare; onSelect: (id: string) => void }) {
      return <li onClick={() => onSelect(fare.id)}>{fare.label}: {fare.price}</li>;
    });
    
    function FareList({ fares }: { fares: Fare[] }) {
      const [query, setQuery] = useState("");
    
      // Stable identity, so memo(FareRow) can skip re-rendering rows while typing.
      const onSelect = useCallback((id: string) => track(id), []);
    
      // Only recomputed when the inputs change.
      const visible = useMemo(
        () => fares.filter((f) => f.label.toLowerCase().includes(query.toLowerCase())),
        [fares, query],
      );
    
      return (
        <>
          <input value={query} onChange={(e) => setQuery(e.target.value)} />
          <ul>
            {visible.map((fare) => (
              <FareRow key={fare.id} fare={fare} onSelect={onSelect} />
            ))}
          </ul>
        </>
      );
    }
    
    export { FareList };

    What the interviewer is checking: Whether you can explain how the three fit together instead of treating each as a magic speed-up.

    Likely follow-up: If FareRow is not wrapped in memo, does the useCallback on onSelect do anything useful?

  12. 12Is wrapping everything in useMemo and useCallback a good habit? When does memoisation hurt?Intermediate

    No. Each memo has a cost: comparing dependencies, holding old values in memory, extra code to read, and the risk of a wrong dependency list that returns stale data. If the saved work is cheaper than that overhead, you have made things slower and harder to read.

    It pays off when a stable reference matters, such as a prop to a memoised child or a dependency of an effect, when you have measured an expensive calculation like sorting thousands of rows, or when a context value object is read by many consumers. It does nothing if the child is not memoised or if the props change anyway.

    Fix the structure first: move state down, split components, or pass heavy parts as children. Verify with the Profiler. Where the React Compiler is adopted, much of this memoisation is automated.

    What the interviewer is checking: Judgement: measured optimisation over cargo-cult memoisation.

    Likely follow-up: How could you stop a slow sibling re-rendering on every keystroke without using memo at all?

  13. 13What is useRef used for, and how does it differ from state?Intermediate

    useRef returns a mutable object, { current }, that persists across renders. Changing current does not trigger a re-render. Use it for two things: holding a DOM node, to focus an input or measure an element, and storing values you need to remember but that do not affect what is on screen, such as a timer ID, the previous value or the latest callback.

    State is a snapshot that triggers a re-render when it changes, and it should drive the UI. A ref is an escape hatch. Do not read or write ref.current during render for anything that changes the output, because React will not update the screen when it changes.

    DOM refs are null until the component is committed, so access them in effects or event handlers.

    What the interviewer is checking: Whether you can separate render-driving data from bookkeeping data.

    Likely follow-up: How would you store the previous value of a prop with a ref, and why does it update one render late?

  14. 14What is a custom hook? When would you write one, and what are the rules?Intermediate

    A custom hook is a function whose name starts with use and that calls other hooks. It reuses stateful logic, not UI, between components. Typical examples are debouncing, subscriptions, form handling or a media-query listener.

    Each call gets its own isolated state, so two components that call the same hook do not share values. If you need sharing, use context or a store. The use prefix matters because the linter then applies the rules of hooks to the function.

    Keep the API small and pass dependencies in as arguments. Pitfall: returning a new object or function on every render forces consumers to re-run effects or defeats memoisation, so return stable references when it matters.

    tsx
    import { useEffect, useState } from "react";
    
    export function useDebouncedValue<T>(value: T, delayMs: number): T {
      const [debounced, setDebounced] = useState(value);
      useEffect(() => {
        const id = setTimeout(() => setDebounced(value), delayMs);
        return () => clearTimeout(id); // a new keystroke cancels the pending update
      }, [value, delayMs]);
      return debounced;
    }
    
    // Usage: the effect that calls the API depends on the debounced text, not the raw text.
    export function ClinicSearch({ search }: { search: (q: string) => void }) {
      const [text, setText] = useState("");
      const debounced = useDebouncedValue(text, 300);
      useEffect(() => {
        if (debounced) search(debounced);
      }, [debounced, search]);
      return <input value={text} onChange={(e) => setText(e.target.value)} />;
    }

    What the interviewer is checking: Whether you can extract logic cleanly and know that hooks share logic but not state.

    Likely follow-up: In the usage above, what happens if the parent passes a new search function on every render?

  15. 15Why must hooks be called at the top level, and not inside conditions or loops?Intermediate

    React does not identify hooks by name. It stores each component's hook state in a list and matches calls by their order during render: first useState, second useEffect, and so on. If a hook is skipped on one render, every later hook reads the wrong slot, and state gets mixed up.

    So the rules are: call hooks in the same order every render, at the top level of a component or custom hook, and only from those places. An early return before a hook also breaks it.

    To make something conditional, put the condition inside the hook, for example an if inside an effect, or move the hook into a child component that you render conditionally. In React 19 the use API is a deliberate exception that can be called conditionally.

    What the interviewer is checking: Whether you know the mechanism behind the rule instead of reciting it.

    Likely follow-up: How would you show a different data-fetching hook depending on a prop, without breaking the rules?

  16. 16Context or a state library like Redux or Zustand: how do you decide?Intermediate

    Context is a way to pass a value down without prop drilling. It is not a state manager. It fits values that are needed widely and change rarely, such as theme, locale or the signed-in user. When the value changes, every component that reads that context re-renders, and there is no selector to subscribe to just a slice.

    So a large, fast-changing object in context causes broad re-renders. You can split contexts or memoise the value, but at some point a store with selectors (Redux Toolkit, Zustand, Jotai) fits better and brings devtools and middleware.

    A sensible order is local state, lifting state up, context for stable app-wide values, then a store for complex shared client state. Server data usually belongs in a data-fetching cache, not copied into either.

    tsx
    import { createContext, useContext } from "react";
    import { renderToStaticMarkup } from "react-dom/server";
    
    const CurrencyContext = createContext<"USD" | "INR">("USD"); // default used without a provider
    
    function Price({ amount }: { amount: number }) {
      const currency = useContext(CurrencyContext);
      return <span>{amount} {currency}</span>;
    }
    
    console.log(
      renderToStaticMarkup(
        <>
          <Price amount={10} />
          <CurrencyContext.Provider value="INR">
            <Price amount={10} />
          </CurrencyContext.Provider>
        </>,
      ),
    );

    Output

    <span>10 USD</span><span>10 INR</span>

    What the interviewer is checking: Whether you know context's re-render behaviour and choose tools by problem, not by fashion.

    Likely follow-up: A context value is an object created inline in the provider. What goes wrong?

  17. 17How does React decide whether to update or recreate a component? Where does key fit in?Intermediate

    During reconciliation React compares the old and new element at the same position in the tree. If the type is the same, such as the same component or the same tag, it updates in place, keeps its state, and compares the children. If the type differs, it unmounts the old subtree, destroying its state, and mounts a new one.

    Two consequences. Defining a component inside another component's body creates a new type on every render, so it remounts each time and loses state and focus; define components at module level. And you can use key deliberately: <ProfileForm key={userId} /> remounts when the user changes, which resets the form cleanly without an effect.

    Keys in lists work the same way: they tell React which element is which when the order changes.

    What the interviewer is checking: Whether you understand identity and position in the tree, and the "change the key to reset" technique.

    Likely follow-up: Why does a component defined inside another component lose its input focus on every keystroke?

  18. 18What do error boundaries catch, and what do they not catch?Intermediate

    An error boundary is a class component that defines getDerivedStateFromError and optionally componentDidCatch. It catches errors thrown while rendering its descendants, in their lifecycle methods and in constructors, and shows a fallback instead of unmounting the whole app.

    It does not catch errors in event handlers (use try/catch there), in asynchronous code such as timers and promises, during server rendering, or in the boundary itself. A rejected promise in a click handler never reaches it.

    Place boundaries at sensible granularity: around a widget or route so the rest of the page keeps working, plus one at the top. Log in componentDidCatch. There is no hook version; libraries like react-error-boundary wrap the class, and frameworks often provide a file convention for it.

    tsx
    import { Component, type ErrorInfo, type ReactNode } from "react";
    
    declare function logToService(error: Error, componentStack: string | null | undefined): void;
    
    type Props = { fallback: ReactNode; children: ReactNode };
    type State = { failed: boolean };
    
    export class WidgetBoundary extends Component<Props, State> {
      state: State = { failed: false };
    
      static getDerivedStateFromError(): State {
        return { failed: true }; // switch to the fallback UI on the next render
      }
    
      componentDidCatch(error: Error, info: ErrorInfo) {
        logToService(error, info.componentStack); // side effects such as logging belong here
      }
    
      render() {
        return this.state.failed ? this.props.fallback : this.props.children;
      }
    }
    
    // <WidgetBoundary fallback={<p>Chart unavailable</p>}><RevenueChart /></WidgetBoundary>

    What the interviewer is checking: Precise knowledge of the boundary's scope, including what slips past it.

    Likely follow-up: How would you let the user retry after the fallback is shown?

  19. 19Explain React.lazy and Suspense. When would you code-split a component?Intermediate

    React.lazy(() => import("./Chart")) creates a component whose code is downloaded the first time it renders. While it loads, the component suspends, and the nearest <Suspense fallback={...}> above it shows the fallback. Once the code arrives, React renders the real component.

    Where you put the boundary decides the experience: one high up gives a whole-page spinner, several low down give per-widget placeholders. Suspense is not limited to lazy loading. Frameworks and data libraries that support it can suspend while data loads, and it is part of streaming server rendering.

    Split heavy or rarely used UI such as chart libraries, editors, modals and admin screens. Do not split tiny components. Combine with an error boundary for failed chunk loads, and note that a plain useEffect fetch does not trigger Suspense.

    What the interviewer is checking: A conceptual model of suspending, with sensible judgement about what to split.

    Likely follow-up: What does the user see if the lazy chunk fails to load, and how do you handle it?

  20. 20How do you test React components? What do you avoid testing?Intermediate

    Test behaviour from the user's point of view. React Testing Library renders the component into a DOM, queries it the way a user finds things, by role, label or text, drives it with user-event, and asserts on what appears. Because the test does not know the internals, you can refactor freely.

    Avoid asserting on state values, internal method calls, class names, or large snapshots, since they break on harmless refactors and prove little. Mock at the network boundary, with a tool like MSW or a stubbed fetch, instead of mocking your own hooks. For asynchronous UI use findBy queries or waitFor.

    Mix test types: unit tests for pure logic and custom hooks, component tests for behaviour, and a few end-to-end tests for critical flows such as checkout. Query by accessible role when possible, which also nudges you toward accessible markup.

    What the interviewer is checking: A testing philosophy centred on user-visible behaviour, plus awareness of the cost of brittle tests.

    Likely follow-up: A test passes but the button is unusable with a keyboard. What would have caught it?

  21. 21Why does my effect run twice in development? Is something broken?Intermediate

    In development, <StrictMode> deliberately mounts a component, unmounts it and mounts it again, so effects run, clean up, and run again. It also calls render functions twice. This does not happen in production.

    The purpose is to expose bugs: an effect without a proper cleanup, or a render that is not pure. If the double run causes a visible problem such as a duplicate subscription or two analytics events, your cleanup is incomplete. The fix is a symmetrical cleanup, for instance unsubscribe or abort the request, not a "has run" ref hack that hides the problem.

    Truly one-time work, such as app initialisation, belongs at module level or in an event handler. This resilience also matters because React may unmount and restore state in newer features.

    What the interviewer is checking: Whether you read StrictMode as a diagnostic tool and fix the cleanup instead of fighting it.

    Likely follow-up: How would you make a chat connection effect safe under this mount, unmount, mount sequence?

  22. 22What are server components and client components? How do you decide where the boundary goes?Advanced

    In frameworks that support React Server Components, such as the Next.js App Router, components run on the server by default. Their code is not shipped to the browser, so they can read a database or secrets directly, use async/await, and shrink the bundle. They cannot use state, effects, event handlers or browser APIs.

    A file marked with "use client" creates a client boundary: that component and everything it imports are bundled for the browser, hydrated, and can be interactive. Client components are still pre-rendered on the server for the first load. Props that cross from server to client must be serialisable.

    A good rule is to keep components on the server and push "use client" down to small interactive leaves. A server component can pass server-rendered content as children into a client component. Details differ by framework and version, so check the docs. See the Next.js course.

    What the interviewer is checking: A clear mental model of what runs where, hedged appropriately for framework differences.

    Likely follow-up: Why can't you pass a plain function as a prop from a server component to a client component?

  23. 23A form feels laggy while typing. Walk me through how you would find and fix the cause.Advanced

    Measure before changing code. Record the interaction in the React DevTools Profiler to see which components render, how long each takes, and why they rendered; use the browser Performance panel for long tasks. Test a production build, since development mode and StrictMode are slower.

    Then fix by cause. State held too high up: move it down or isolate the input in its own component. Children re-rendering needlessly: pass them as children, or memoise with stable props. Expensive derived data: useMemo. Huge lists: virtualise them so only visible rows render. Heavy but non-urgent updates: useDeferredValue or startTransition.

    Also check for inline context values and effects that set state in a chain. Re-measure after each change, and stop once the interaction feels responsive; the target is the user's experience, not zero re-renders.

    What the interviewer is checking: A methodical measure-diagnose-fix approach, not a list of memoisation tricks.

    Likely follow-up: The Profiler shows one component taking 200 ms to render. What do you check inside it?

  24. 24What do useTransition and useDeferredValue do? When would you use each?Advanced

    Both are concurrent rendering features from React 18. They let you mark some updates as non-urgent so that urgent ones, like typing and clicking, stay responsive. React can interrupt the background render if a new urgent update arrives.

    useTransition gives you startTransition and isPending; state updates inside the callback are treated as a transition. Use it when you control the update, such as switching a tab. useDeferredValue takes a value and returns a version that may lag behind: React first renders with the old value, then re-renders in the background with the new one. Use it when the value comes from props or an input you do not want to wrap.

    Neither makes a slow render faster, and neither debounces network requests. They only make the work interruptible.

    tsx
    import { useDeferredValue, useMemo, useState } from "react";
    
    function ResultList({ items }: { items: string[] }) {
      return (
        <ul>
          {items.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      );
    }
    
    export function RecipeSearch({ recipes }: { recipes: string[] }) {
      const [query, setQuery] = useState("");
      const deferredQuery = useDeferredValue(query); // may lag behind query while React is busy
    
      const results = useMemo(
        () => recipes.filter((r) => r.toLowerCase().includes(deferredQuery.toLowerCase())),
        [recipes, deferredQuery],
      );
    
      return (
        <>
          <input value={query} onChange={(e) => setQuery(e.target.value)} />
          <ResultList items={results} />
        </>
      );
    }

    What the interviewer is checking: Whether you understand that these prioritise work rather than reduce it, and know their limits.

    Likely follow-up: How is useDeferredValue different from debouncing the input?

  25. 25Fetching data in useEffect can show stale or wrong results. Why, and how do you make it safe?Advanced

    Requests can finish out of order. If the user switches from one route to another and the first response arrives last, the screen shows data for the wrong input. The same problem appears when a component unmounts before the response lands.

    Return a cleanup from the effect that cancels the request with an AbortController, or sets an ignore flag so late results are dropped. Reset or show a loading state when inputs change, and handle errors separately from aborts. The double mount in StrictMode is a good test for this.

    In real applications, prefer a data layer such as TanStack Query or SWR, or your framework's loaders. They add caching, de-duplication, retries and race handling that are tedious and error-prone to rebuild by hand.

    tsx
    import { useEffect, useState } from "react";
    
    type Quote = { total: number };
    
    export function useRideQuote(pickup: string, drop: string) {
      const [quote, setQuote] = useState<Quote | null>(null);
      const [error, setError] = useState<string | null>(null);
    
      useEffect(() => {
        const controller = new AbortController();
        setQuote(null);
        setError(null);
    
        const params = new URLSearchParams({ from: pickup, to: drop });
        fetch("/api/quote?" + params, { signal: controller.signal })
          .then((res) => {
            if (!res.ok) throw new Error("HTTP " + res.status);
            return res.json() as Promise<Quote>;
          })
          .then(setQuote)
          .catch((err: unknown) => {
            if (err instanceof DOMException && err.name === "AbortError") return; // superseded request
            setError(err instanceof Error ? err.message : "failed");
          });
    
        return () => controller.abort(); // runs before the next effect and on unmount
      }, [pickup, drop]);
    
      return { quote, error };
    }

    What the interviewer is checking: Whether you can reason about asynchronous races and know when to hand the problem to a library.

    Likely follow-up: What does a library like TanStack Query give you that this hook does not?

  26. 26What is a hydration mismatch, what causes it, and how do you fix it?Advanced

    With server rendering, the browser first receives HTML, then React hydrates it by attaching to the existing DOM. React expects the first client render to produce the same output as the server did. A mismatch means they differ.

    Typical causes: values that change between server and client such as Date.now(), Math.random() or a date formatted in a different locale or time zone; branching on typeof window or localStorage during render; invalid HTML nesting such as a <div> inside a <p>; and browser extensions that modify the page. You get a warning in development, and React may patch or discard the server HTML, causing flicker.

    Fixes: compute browser-only values after mount in an effect, starting from a server-safe initial state; use useId for ids; pass timestamps and locale from the server; use suppressHydrationWarning only for a single unavoidable text value; or load browser-only widgets without server rendering.

    What the interviewer is checking: Understanding of the server/client rendering contract and concrete debugging habits.

    Likely follow-up: A price shows the correct currency on the server but flashes a different one after load. Where do you look first?

  27. 27You are building a reusable Tabs or Modal component for many teams. How do you design its API?Advanced

    Prefer composition over configuration. Instead of a dozen boolean and string props, expose parts that callers arrange themselves, such as Tabs, Tabs.List and Tabs.Panel, sharing state through context internally. When callers need full control over rendering, offer a headless hook or a render prop.

    Support both controlled use (value plus onChange) and uncontrolled use (defaultValue). Forward refs and spread remaining props onto the root element, and build in accessibility: roles, keyboard navigation and focus management. Use TypeScript discriminated unions so invalid prop combinations do not compile.

    The pitfall is a growing surface of flags like variant, isCompact and hasFooter, which is hard to test and impossible to extend without breaking changes. Keep the public API small and stable, and document it with examples.

    What the interviewer is checking: Design judgement for shared components: composition, controlled and uncontrolled modes, accessibility.

    Likely follow-up: How would you let consumers customise styling without exposing every internal class name?

Fill the gaps with a free course

If one of these React answers felt shaky, these 3 courses cover the same ground step by step: React, Next.js, JavaScript Fundamentals.

More interview questions

Practising React for a real opening? Upload your resume to see which open jobs list React skills you already have, or browse open roles.