InterviewHack.ai
Empezar gratis
Blog/React Developer Interview Questions and How to Answer Them (50+ Questions)

React Developer Interview Questions and How to Answer Them (50+ Questions)

August 9, 2026

reactjavascript
React Developer Interview Questions and How to Answer Them (50+ Questions)

Comprehensive React developer interview guide covering 55+ questions across hooks, performance, Virtual DOM/Fiber, state management, testing, TypeScript, and concurrent features — with real code examples and examiner-ready answers.

React Developer Interview Questions and How to Answer Them (50+ Questions)

Whether you are interviewing for a junior, mid-level, or senior React role, this guide covers every topic an interviewer is likely to raise — from first-principles fundamentals to the architecture decisions that separate experienced engineers from the rest. Each answer is written the way a strong candidate would actually speak it, with working code you can study and adapt.

The questions are grouped by topic so you can drill the areas where you need the most work.


Table of Contents

  1. 1Core Fundamentals (Q1–Q8)
  2. 2React Hooks Deep Dive (Q9–Q20)
  3. 3Performance Optimization (Q21–Q28)
  4. 4Virtual DOM, Fiber, and Reconciliation (Q29–Q34)
  5. 5State Management (Q35–Q40)
  6. 6Testing (Q41–Q45)
  7. 7TypeScript with React (Q46–Q48)
  8. 8Concurrent Features and React 18/19 (Q49–Q52)
  9. 9Architecture and Patterns (Q53–Q55)

Part 1 — Core Fundamentals

Q1. What is React and what problem does it solve?

React is a JavaScript library for building user interfaces. It was created at Facebook to solve a very specific problem: keeping a complex, data-driven UI in sync with application state without manually updating the DOM every time something changes.

The two core ideas that make React work:

Declarative rendering. You describe what the UI should look like for a given state. React figures out how to make the DOM match. You never write "find this node and change its text."

Component model. UIs are composed of small, reusable, self-contained pieces. Each component owns its own logic, markup, and (optionally) its own state. This makes large codebases easier to reason about and test.

jsx
// Declarative — describe what you want, not how to achieve it
function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}

Interview tip: Mention that React is a *library*, not a framework. It handles the view layer; routing, data fetching, and state management require separate decisions.


Q2. What is JSX and how does it compile?

JSX is a syntax extension that looks like HTML inside JavaScript. It is not valid JavaScript — the build step (Babel or the React compiler) transforms it into React.createElement calls.

jsx
// What you write
const el = <button className="btn">Click me</button>;

// What it compiles to (pre-React 17 transform)
const el = React.createElement("button", { className: "btn" }, "Click me");

// With the new JSX transform (React 17+), no import needed
// The compiler imports from react/jsx-runtime automatically

JSX is entirely optional but nearly universal because it is more readable than nested createElement calls. It enforces one important rule: adjacent elements must be wrapped in a single parent (or <>... Fragment) because a function can only return one value.


Q3. What is the difference between a controlled and uncontrolled component?

Controlled component: React state is the single source of truth. Every keystroke updates state, and the input's value is always derived from that state.

jsx
function ControlledInput() {
  const [value, setValue] = React.useState("");

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

Uncontrolled component: The DOM itself holds the value. You read it via a ref when you need it (e.g., on form submit).

jsx
function UncontrolledInput() {
  const inputRef = React.useRef(null);

  function handleSubmit() {
    console.log(inputRef.current.value);
  }

  return <input ref={inputRef} />;
}

When to use which:

  • Controlled: form validation on every keystroke, dependent fields, complex form state.
  • Uncontrolled: simple file inputs, integrating with non-React libraries, or when you want minimal re-renders on every keystroke (large forms with hundreds of fields).

Q4. Explain the React component lifecycle for both class components and functional components with hooks.

Class component lifecycle (simplified):

| Phase | Methods |

|---|---|

| Mount | constructor → render → componentDidMount |

| Update | render → componentDidUpdate |

| Unmount | componentWillUnmount |

Functional component equivalent with hooks:

jsx
function MyComponent({ id }) {
  // Runs on every render (like render())
  console.log("rendering");

  React.useEffect(() => {
    // Runs after mount (like componentDidMount)
    const subscription = subscribe(id);

    return () => {
      // Runs before unmount or before next effect (like componentWillUnmount)
      subscription.cancel();
    };
  }, [id]); // Re-runs when id changes (like componentDidUpdate for id)
}

Interviewers care about three things here: knowing the cleanup return, understanding the dependency array, and knowing that useLayoutEffect fires synchronously before the browser paints (use it for DOM measurements).


Q5. What are props and how do they differ from state?

| | Props | State |

|---|---|---|

| Owned by | Parent component | The component itself |

| Mutable? | No (read-only from child's perspective) | Yes, via setter |

| Triggers re-render? | Yes, when parent passes new value | Yes, when updated |

| Purpose | Configuration, communication down the tree | Local dynamic data |

jsx
// Props — passed in, read-only
function UserCard({ name, role }) {
  return <div>{name} — {role}</div>;
}

// State — owned locally, mutable
function Counter() {
  const [count, setCount] = React.useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

A common follow-up: "Can a child modify its own props?" — No. The child calls a callback prop provided by the parent, and the parent updates its own state, which flows back down as new props.


Q6. What are React Fragments and why do they exist?

React.Fragment (or the shorthand <>...) lets you return multiple elements from a component without adding an extra DOM node.

jsx
// Without Fragment — adds a meaningless div to the DOM
function TableRow() {
  return (
    <div>
      <td>Name</td>
      <td>Age</td>
    </div>
  );
}

// With Fragment — valid HTML structure preserved
function TableRow() {
  return (
    <>
      <td>Name</td>
      <td>Age</td>
    </>
  );
}

Use the explicit form (not the shorthand) when you need to add a key prop to items in a list.


Q7. What is prop drilling and how do you avoid it?

Prop drilling happens when a deeply nested component needs data and you pass it through every intermediate component even though those intermediates don't use the data themselves.

App (has user) → Layout → Sidebar → UserMenu (needs user)
                        ↑ Layout and Sidebar don't need user
                          but have to pass it along

Solutions in order of complexity:

  1. 1Context API — good for infrequently changing data (theme, locale, auth user).
  2. 2Component composition — lift the consumer component up and pass it as a child or prop (often underused).
  3. 3External state library (Zustand, Redux) — components subscribe directly to the store slice they need.

Q8. What is a React Portal?

Portals render a component's output into a different DOM node than its parent, while keeping it in the React tree (so events still bubble through React's synthetic event system).

jsx
import { createPortal } from "react-dom";

function Modal({ children, isOpen }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content" role="dialog" aria-modal="true">
        {children}
      </div>
    </div>,
    document.getElementById("modal-root") // target DOM node outside #root
  );
}

Use cases: modals, tooltips, dropdown menus that need to break out of overflow: hidden containers, and toast notifications.


Part 2 — React Hooks Deep Dive

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

React tracks hook state by call order, not by name. That means hook #1 always corresponds to the same piece of state across renders. If you call hooks conditionally, the order can change between renders and React loses track of which state belongs to which hook.

Rule 1: Only call hooks at the top level — never inside loops, conditions, or nested functions.

Rule 2: Only call hooks from React function components or other custom hooks.

jsx
// WRONG — conditional hook call
function Component({ show }) {
  if (show) {
    const [value, setValue] = useState(""); // hook #1 only sometimes
  }
  const [other, setOther] = useState(0); // might be hook #1 or #2 depending on show
}

// CORRECT — the condition is inside the hook, not around it
function Component({ show }) {
  const [value, setValue] = useState("");
  const [other, setOther] = useState(0);

  if (!show) return null;
}

The eslint-plugin-react-hooks package enforces these rules automatically at lint time.


Q10. How does useState work? What is lazy initialization?

useState returns a state variable and a setter. React persists the state between renders; calling the setter schedules a re-render with the new value.

Functional updater pattern — use this when the new state depends on the previous value to avoid stale reads:

jsx
// Potentially stale (reads count from closure)
setCount(count + 1);

// Safe — always uses the latest state
setCount(prev => prev + 1);

Lazy initialization — when initial state is expensive to compute, pass a function instead of a value. The function runs only on the first render:

jsx
// Without lazy init — parses JSON on EVERY render
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings")));

// With lazy init — parses JSON once
const [settings, setSettings] = useState(() => JSON.parse(localStorage.getItem("settings")));

Q11. Explain the useEffect hook and its dependency array behavior.

useEffect runs a side effect (data fetching, subscriptions, DOM manipulation) after the component renders.

jsx
useEffect(() => {
  // Side effect code

  return () => {
    // Cleanup — runs before next effect or unmount
  };
}, [dependency1, dependency2]);

Dependency array behavior:

| Array | Behavior |

|---|---|

| Not provided | Runs after every render |

| [] (empty) | Runs once after mount |

| [a, b] | Runs after mount and after any render where a or b changed |

Data fetching with cleanup to prevent race conditions:

jsx
useEffect(() => {
  let active = true;

  async function fetchUser() {
    const data = await getUser(userId);
    if (active) {
      setUser(data);
    }
  }

  fetchUser();

  return () => {
    active = false; // Ignore stale responses if userId changes before this resolves
  };
}, [userId]);

Modern React with React 19's use hook or a library like TanStack Query handles this race-condition problem for you at a higher level.


Q12. What is useLayoutEffect and when do you use it instead of useEffect?

| | useEffect | useLayoutEffect |

|---|---|---|

| When it runs | Asynchronously after browser paint | Synchronously after DOM mutations, before paint |

| Use case | Most side effects | DOM measurements, preventing visual flicker |

jsx
function Tooltip({ anchor }) {
  const tooltipRef = useRef(null);
  const [position, setPosition] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    // Read DOM measurements BEFORE the browser paints
    // so there's no visible "jump"
    const rect = anchor.getBoundingClientRect();
    setPosition({ top: rect.bottom, left: rect.left });
  }, [anchor]);

  return (
    <div ref={tooltipRef} style={{ position: "absolute", ...position }}>
      Tooltip content
    </div>
  );
}

Rule of thumb: default to useEffect. Only reach for useLayoutEffect when you have a measurable visual flicker caused by layout reads immediately after render.


Q13. What is useRef and what are its two main use cases?

useRef returns a mutable object { current: value } that persists across renders without triggering re-renders when mutated.

Use case 1 — DOM access:

jsx
function FocusInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus</button>
    </>
  );
}

Use case 2 — Mutable values that should not trigger re-renders (e.g., timers, previous values):

jsx
function Interval() {
  const timerRef = useRef(null);

  function start() {
    timerRef.current = setInterval(() => console.log("tick"), 1000);
  }

  function stop() {
    clearInterval(timerRef.current);
  }

  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

Q14. Explain useMemo and useCallback. When should you actually use them?

Both are memoization tools that exist to preserve referential identity across renders.

useMemo — memoizes the *result* of a computation:

jsx
const filteredList = useMemo(
  () => items.filter(item => item.active && item.name.includes(query)),
  [items, query] // Recompute only when these change
);

useCallback — memoizes the *function itself*:

jsx
const handleDelete = useCallback(
  (id) => dispatch({ type: "DELETE", id }),
  [dispatch] // dispatch from useReducer is stable, so this runs once
);

When to actually use them:

  1. 1useMemo: expensive computations (O(n log n) or heavier), or when an object/array reference must be stable for a dependency array.
  2. 2useCallback: the function is passed to a React.memo-wrapped child component that would otherwise re-render, or the function is a dependency of another hook.

When NOT to use them: wrapping every function and value blindly adds memory and comparison overhead. Profile first with React DevTools Profiler, then memoize only where there is a measured problem.


Q15. What is useReducer and when is it better than useState?

useReducer manages state via a pure function (the reducer) that maps (state, action) => newState. It follows the same pattern as Redux.

jsx
const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + state.step };
    case "SET_STEP":
      return { ...state, step: action.payload };
    case "RESET":
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>Count: {state.count} (step: {state.step})</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={() => dispatch({ type: "SET_STEP", payload: 5 })}>Step 5</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
    </>
  );
}

Prefer useReducer over useState when:

  • Multiple related state values change together.
  • Next state depends on previous state in non-trivial ways.
  • You want co-located, testable state logic (the reducer is a pure function — easy to unit test with no React needed).
  • You find yourself writing many useState setters that always change together.

Q16. How do you write a custom hook? Give a realistic example.

A custom hook is a JavaScript function whose name starts with use and that calls other hooks internally. It lets you extract and share stateful logic without adding extra components to the tree.

jsx
// useFetch — data fetching with loading, error, and cancellation
function useFetch(url) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      try {
        setLoading(true);
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        setData(json);
      } catch (err) {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch(`/api/users/${id}`);

  if (loading) return <Spinner />;
  if (error) return <p>Error: {error}</p>;
  return <div>{user.name}</div>;
}

Q17. What is a stale closure in React and how do you fix it?

A stale closure happens when an effect (or event handler) captures a variable from the time it was created, and that variable later goes out of date without the closure knowing.

jsx
// BUG — stale closure in setInterval
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      // count is always 0 here — captured at mount, never updates
      console.log(count);
      setCount(count + 1); // will always set to 1
    }, 1000);

    return () => clearInterval(interval);
  }, []); // Empty array means effect never re-runs

  return <div>{count}</div>;
}

Fix 1 — functional updater (for state):

jsx
setCount(prev => prev + 1); // reads current state, not closure value

Fix 2 — ref to hold latest value (for anything else):

jsx
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count; // Always up to date

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(countRef.current); // Never stale
    }, 1000);

    return () => clearInterval(interval);
  }, []);
}

Q18. What is useContext and what are its performance pitfalls?

useContext subscribes a component to a context value. When the context value changes, every component that called useContext with that context re-renders — even if they only use part of the value.

jsx
const ThemeContext = React.createContext("light");

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
        Toggle
      </button>
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>Click</button>;
}

Performance pitfall: if you put { user, setUser, theme, setTheme } all in one context, every consumer re-renders whenever any of those values change.

Fixes:

  1. 1Split contexts by concern (one for user, one for theme).
  2. 2Memoize the context value: const value = useMemo(() => ({ user }), [user]).
  3. 3For frequently changing values, reach for Zustand's useStore(selector) — it only re-renders when the selected slice changes.

Q19. Explain useTransition and useDeferredValue (React 18+).

These hooks allow you to mark certain state updates as non-urgent so React can keep the UI responsive during heavy renders.

useTransition — wraps the state setter at the call site:

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

  function handleChange(e) {
    setQuery(e.target.value); // Urgent — update the input immediately

    startTransition(() => {
      setResults(heavyFilter(e.target.value)); // Non-urgent — can be interrupted
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList results={results} />
    </>
  );
}

useDeferredValue — wraps a value at the consumption site (use when the value comes from props you do not control):

jsx
function ResultList({ query }) {
  const deferredQuery = useDeferredValue(query);
  // Renders with a lagging value while React prioritizes urgent updates
  const results = useMemo(() => heavyFilter(deferredQuery), [deferredQuery]);
  return <List items={results} />;
}

Key difference: useTransition is for when you control the setter. useDeferredValue is for when you receive a value as a prop and want to defer downstream computation.


Q20. What is useId and why does it exist?

useId generates a unique, stable ID that is consistent between server and client renders. It exists specifically to prevent hydration mismatches when you need IDs for accessibility (linking to ).

jsx
function FormField({ label }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}

Never use this to generate list keys — use the data's own identifier for that. useId is purely for accessibility wiring.


Part 3 — Performance Optimization

Q21. What is React.memo and how does it differ from useMemo?

React.memo is a higher-order component that memoizes an entire functional component. It does a shallow comparison of props before deciding whether to re-render.

jsx
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  console.log("ExpensiveList rendered");
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

// In the parent — useCallback keeps onSelect stable across renders
function Parent() {
  const [selected, setSelected] = useState(null);

  const handleSelect = useCallback((id) => {
    setSelected(id);
  }, []);

  return <ExpensiveList items={STATIC_ITEMS} onSelect={handleSelect} />;
}

Key distinction:

  • React.memo — memoizes a component (prevents re-render).
  • useMemo — memoizes a value (prevents recomputation).

React.memo only helps if the component's props are actually stable. If you pass a new object or function literal on every parent render, React.memo does nothing. That is why useCallback and useMemo are often used together with it.


Q22. How does code splitting work with React.lazy and Suspense?

Code splitting breaks your bundle into chunks that load on demand. React.lazy loads a component only when it is first rendered.

jsx
import React, { lazy, Suspense } from "react";

// Dashboard is loaded in a separate chunk — not included in initial bundle
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
  const [page, setPage] = useState("home");

  return (
    <Suspense fallback={<div>Loading...</div>}>
      {page === "dashboard" && <Dashboard />}
      {page === "settings" && <Settings />}
    </Suspense>
  );
}

For route-level splitting (the most impactful split point), pair it with React Router:

jsx
const routes = [
  { path: "/dashboard", element: lazy(() => import("./pages/Dashboard")) },
  { path: "/settings", element: lazy(() => import("./pages/Settings")) },
];

Interview follow-up: "What do you split?" Route-level components first. Then large, rarely-used features like rich text editors, chart libraries, or modals containing heavy third-party dependencies.


Q23. What is virtualization (windowing) and when do you use it?

When you render a list of 10,000 items, React creates 10,000 DOM nodes — most of which are off-screen. Virtualization renders only the items currently visible in the viewport, dramatically reducing DOM nodes.

Libraries: react-window (lightweight), react-virtual (TanStack Virtual — more flexible), @tanstack/react-virtual.

jsx
import { FixedSizeList } from "react-window";

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Rule of thumb: lists over ~200 items are candidates for virtualization. Profile with React DevTools first.


Q24. How do you identify performance bottlenecks in a React app?

Step 1 — React DevTools Profiler. Record a session, look at the flamegraph. Bars that are wide and orange are slow. Look for components that re-render when they should not.

Step 2 — Check why components re-render. Right-click a component in DevTools → "Highlight updates when components render." Unexpected flashes indicate unnecessary re-renders.

Step 3 — Browser performance tab. Look for long tasks (>50ms on the main thread) blocking interaction.

Common culprits:

  • Parent re-renders pass new object/array literals as props to memoized children (breaking memoization).
  • Large context values causing entire subtrees to re-render on every change.
  • Missing keys on lists causing full unmounts and remounts.
  • Expensive computations inside render without useMemo.
  • Fetching data without caching (re-fetching on every mount).

Q25. What is the difference between shallow and deep comparison? Why does React use shallow comparison?

Shallow comparison compares primitive values by value and objects/arrays by reference:

{ a: 1 } === { a: 1 } → false (different references)
const obj = { a: 1 }; obj === obj → true (same reference)

Deep comparison recursively checks every nested value — correct but O(n) in the size of the data structure.

React uses shallow comparison in React.memo, PureComponent, and dependency array checks because it is O(1) and predictable. Deep comparison would be too expensive to run on every render for every prop.

The practical implication: if you pass a new object/array literal as a prop, shallow comparison always says "changed" even if the content is identical. The fix is to memoize the object with useMemo or move it outside the component if it is static.


Q26. How do you optimize a React app's initial load time?

  1. 1Code splitting at route and feature level (React.lazy + Suspense).
  2. 2Tree shaking — import only what you use: import { debounce } from "lodash-es" not import _ from "lodash".
  3. 3Preload critical resources with for fonts and key images.
  4. 4Compress assets — Brotli/gzip via CDN or server config.
  5. 5Defer non-critical JS —