← All practice sets
FrontendPractice · 12 questions

React interview questions

The React questions interviewers keep asking: rendering and reconciliation, keys, state and props, effects and their cleanup, stale closures, memoization and the React Compiler, context, forms, Suspense, Server Components, error boundaries, and a component to review.

Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.

  1. Q1Junior

    Walk through what happens after you call `setState` in a React component, from the update to the DOM change.

    Reveal model answer

    Calling the setter queues an update and schedules a render; React batches updates made in the same event, so several setters usually produce one render. In the render phase React calls the component function (and, by default, every child it renders) to get a new element tree; this must be pure. Reconciliation then compares the new tree with the previous one: elements of the same type at the same position are updated in place and keep their state, while a different type at that position tears down the old subtree and mounts a new one. In the commit phase React applies only the minimal DOM changes, then runs layout effects and, after paint, regular effects.

    WHAT A STRONG ANSWER COVERS

    • Render (calling components, pure) is separate from commit (touching the DOM).
    • Updates in the same event are batched into one render.
    • A parent re-render re-renders its children by default, whether or not their props changed.
    • Reconciliation matches by element type and position (plus key), not by deep comparison of output.
    • Re-rendering is not the same as changing the DOM; unchanged output means no DOM writes.

    RED FLAGS

    • "React re-renders a component only when its props change."
    • Saying React diffs the real DOM, or that the virtual DOM is always faster than direct DOM updates.
    • Expecting the state variable to hold the new value on the line after calling the setter.

    LIKELY FOLLOW-UPS

    • Why does `setCount(count + 1)` called three times in one handler only add one?
    • What happens to state when you wrap a component in a different parent element on some renders?

    SOURCES

  2. Q2Mid

    A list of editable rows uses `key={index}`. A user types into row 2, then deletes row 1, and their text appears in the wrong row. Why, and how do keys also help you reset a component on purpose?

    Reveal model answer

    Keys tell React which element is which between renders among siblings; with index keys, deleting the first row shifts every later item down one index, so each key now points at different data while React keeps the component instances and DOM inputs that had those keys; the text typed into the second row's uncontrolled input (key 1) is now shown beside the item that used to be third, and the last instance is the one removed. Use a stable, unique ID from the data, never the index for lists that can reorder, insert or delete, and never `Math.random()`, which remounts every row on every render and loses input and focus. Because a key is part of a component's identity, changing it deliberately destroys and recreates the component: `<EditContact key={contact.id} contact={contact} />` resets the form's internal state when the selected contact changes, without an effect.

    WHAT A STRONG ANSWER COVERS

    • Keys identify elements among siblings, not globally.
    • Index keys break when the list order changes; they are fine only for static lists.
    • Random or render-generated keys remount everything, losing state, focus and performance.
    • Changing a key on purpose is the idiomatic way to reset a subtree's state.

    RED FLAGS

    • "Keys are only there to silence the console warning."
    • Generating IDs with `crypto.randomUUID()` during render.
    • Syncing a prop into state with an effect to reset a form instead of using a key.

    LIKELY FOLLOW-UPS

    • The data has no IDs. Where do you generate them so they stay stable?

    SOURCES

  3. Q3Junior

    What is the difference between state and props, and what is wrong with `const [name, setName] = useState(props.name);` in a display component?

    Reveal model answer

    Props are inputs a parent passes in and the component must treat them as read-only; state is private memory owned by one component instance, which only that component can change and which persists across renders at that position in the tree. Copying a prop into state captures only the initial value: when the parent later passes a different `name`, the component keeps showing the old one, because `useState` ignores its argument after the first render. If the value can be computed from props or other state, compute it during render; if two components need the same changing value, lift the state to their closest common parent and pass it down. Copying into state is right only when the prop is explicitly an initial value, such as `initialName`, for an editable draft.

    WHAT A STRONG ANSWER COVERS

    • Props flow down and are read-only; state is local and private to a component instance.
    • `useState(initial)` only reads its argument on the first render.
    • Derive values during render instead of storing redundant state.
    • Lift state up to share it; keep a single source of truth.

    RED FLAGS

    • Mutating props or state objects directly.
    • Fixing the copied prop with a `useEffect` that calls `setName(props.name)`.
    • Putting everything into global state to avoid passing props.

    LIKELY FOLLOW-UPS

    • When would you name a prop `initialValue` or `defaultValue`, and why?

    SOURCES

  4. Q4Mid

    What is wrong with `useEffect(() => { fetch("/api/users/" + id).then(r => r.json()).then(setUser); }, [id]);`, and why does React run effects twice in development?

    Reveal model answer

    It has no cleanup, so if `id` changes quickly the responses can arrive out of order and an older user overwrites the newer one; it also sets state after unmount and wastes requests. Return a cleanup that sets an `ignore` flag or aborts an `AbortController`, and only call `setUser` if the effect is still current. React runs cleanup before the effect re-runs with new dependencies and once on unmount, and in development Strict Mode deliberately mounts, unmounts and remounts every component once so missing cleanup shows up as a visible bug. The fix is a correct cleanup, not a ref that blocks the second run; for real apps a framework's data loading or a cache library such as TanStack Query handles caching, deduplication and races for you.

    WHAT A STRONG ANSWER COVERS

    • Every effect that starts something needs a matching stop: connect/disconnect, subscribe/unsubscribe, fetch/ignore or abort.
    • Cleanup runs before the next effect run and on unmount.
    • Out-of-order responses are a real race; `ignore` or `AbortController` fixes it.
    • Strict Mode's double run in development is intentional and does not happen in production.
    • Effects synchronize with external systems; derived data and user-event logic do not belong in effects.

    RED FLAGS

    • Using a `useRef(false)` guard to stop the second development run.
    • Disabling the exhaustive-deps lint rule to make the effect run once.
    • Making the effect callback itself `async`, so it returns a promise instead of a cleanup.

    LIKELY FOLLOW-UPS

    • When is `useLayoutEffect` the right choice over `useEffect`?
    • Why might you not fetch in an effect at all?

    SOURCES

  5. Q5Mid

    This counter stops at 1: `useEffect(() => { const t = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(t); }, []);`. Explain why and give two fixes.

    Reveal model answer

    Each render's functions close over that render's props and state, a snapshot; with an empty dependency array the effect runs once, so its interval callback forever sees `count` as 0 and keeps setting it to 1. The simplest fix here is the updater form `setCount(c => c + 1)`, which reads the latest queued state and lets the effect keep no dependency on `count`. The alternative is to list `count` as a dependency so the interval is recreated each change, which works but resets the timer. When an effect needs to read the latest value of something without re-running, React 19.2 added `useEffectEvent`: move that logic into an effect event, call it from the effect, and leave it out of the dependency array.

    WHAT A STRONG ANSWER COVERS

    • State is a snapshot per render; closures capture that render's values.
    • An empty dependency array that reads changing values is the usual cause.
    • Updater functions avoid the need to read current state.
    • `useEffectEvent` (React 19.2+) reads latest values without making them reactive; a ref is the older workaround.
    • Keep the exhaustive-deps lint rule on; it catches most of these.

    RED FLAGS

    • "The interval is slow" or blaming batching.
    • Silencing the lint rule instead of fixing the dependency.
    • Using `useEffectEvent` to hide a value that should re-trigger the effect.

    LIKELY FOLLOW-UPS

    • Where else do stale closures appear outside effects, for example in a `setTimeout` in an event handler?

    SOURCES

  6. Q6Senior

    When do `memo`, `useMemo` and `useCallback` actually help, and how does the React Compiler change that advice?

    Reveal model answer

    `memo` lets React skip re-rendering a component when its props are equal by `Object.is`; `useMemo` caches a computed value and `useCallback` caches a function between renders, mostly so references stay stable for memoized children or effect dependencies. They help only when a render is measurably expensive and props are actually stable; one inline object or arrow function prop defeats `memo`, and memoizing cheap work just adds overhead. Cheaper first steps are moving state down, passing JSX as `children`, and keeping renders pure. React Compiler 1.0, stable since October 2025 and working with React 17 and later, memoizes components and values automatically at build time when code follows the Rules of React, so new code mostly does not need manual memoization; the React docs suggest keeping existing `useMemo` and `useCallback` until you have tested removing them, and using them as an escape hatch, for example for an effect dependency.

    WHAT A STRONG ANSWER COVERS

    • `memo` compares props shallowly; unstable object or function props defeat it.
    • Profile first; memoization is a performance hint, not a correctness tool.
    • Structural fixes (state colocation, `children` composition) often beat memoization.
    • React Compiler is stable (1.0, October 2025), automatic, and relies on the Rules of React.
    • With the compiler, manual hooks become escape hatches; removing existing ones can change output.

    RED FLAGS

    • Wrapping every function in `useCallback` by default.
    • Using `useMemo` to make code correct, for example to run a side effect once.
    • Claiming the compiler is still experimental, or that it removes the need to follow the Rules of React.

    LIKELY FOLLOW-UPS

    • How would you find out which components re-render too often?
    • What kinds of code can the compiler not optimize, and how would you notice?

    SOURCES

  7. Q7Mid

    An app puts `{ user, theme, cart, setCart }` into one context provider at the root. Typing in the cart makes the whole app sluggish. Why, and how would you fix it?

    Reveal model answer

    Every component that reads a context re-renders when the provider's value changes by `Object.is`, and `memo` on an intermediate component does not stop that. A single object literal created on every render of the root is a new value each time, and because it mixes fast-changing cart state with rarely changing user and theme, every consumer re-renders on every keystroke. Fixes: split contexts by how often they change (and separate state from stable setters), memoize the value with `useMemo` and `useCallback` (or let the React Compiler do it), keep fast-changing state local to where it is used, and for high-frequency shared state use a store with selector subscriptions such as Zustand or `useSyncExternalStore`.

    WHAT A STRONG ANSWER COVERS

    • Consumers re-render when the provider value changes by reference.
    • `memo` does not block context updates to consumers.
    • Split contexts by update frequency; separate data from dispatch functions.
    • Stabilize the value object; context is for low-frequency, widely needed values.
    • Selector-based stores let components subscribe to only the slice they use.

    RED FLAGS

    • "Context is slow, never use it."
    • Assuming only components that use the changed field re-render.
    • Moving everything to a global store without measuring.

    LIKELY FOLLOW-UPS

    • How does `useSyncExternalStore` avoid tearing with concurrent rendering?

    SOURCES

  8. Q8Junior

    What is the difference between controlled and uncontrolled inputs, and why does React warn "A component is changing an uncontrolled input to be controlled"?

    Reveal model answer

    A controlled input gets its current `value` (or `checked`) from React state and reports changes through `onChange`, so state is the single source of truth and you can validate, format or reset on every keystroke. An uncontrolled input keeps its own value in the DOM; you set an initial value with `defaultValue` and read it on submit through a ref or `FormData`, which is less code and fewer renders. The warning appears when `value` starts as `undefined` or `null`, so React treats the input as uncontrolled, and later becomes a string; initialize state to `''` or use `value={x ?? ''}`. Passing `value` without `onChange` makes a read-only field, so add `onChange`, use `defaultValue`, or mark it `readOnly`.

    WHAT A STRONG ANSWER COVERS

    • Controlled: `value` plus `onChange`; uncontrolled: `defaultValue` and read on submit.
    • An input must not switch between the two during its lifetime.
    • `undefined` or `null` as `value` makes it uncontrolled.
    • Uncontrolled with `FormData` (or form actions in React 19) suits simple forms; controlled suits live validation and dependent fields.

    RED FLAGS

    • "Uncontrolled inputs are bad practice."
    • Setting `value` with no `onChange` and wondering why typing does nothing.
    • Reading input values with `document.querySelector` in a React app.

    LIKELY FOLLOW-UPS

    • How do form libraries such as React Hook Form keep large forms fast?

    SOURCES

  9. Q9Senior

    How does Suspense work for data fetching? Why doesn't wrapping a component that fetches in `useEffect` in `<Suspense>` show the fallback?

    Reveal model answer

    A `<Suspense>` boundary shows its fallback while something inside it suspends: lazy-loaded code via `lazy`, a cached promise read with `use`, data from Server Components or a Suspense-enabled framework or library, and content still streaming during SSR. Suspense does not detect fetches started in an effect or event handler, so the component simply renders its empty state and then updates later. The promise passed to `use` must be cached and reused across renders, not created during render, otherwise every render starts a new request. Wrapping navigation in `startTransition` (or using `useDeferredValue`) keeps already revealed content on screen instead of flashing back to a fallback, and nested boundaries let you choose where loading states appear.

    WHAT A STRONG ANSWER COVERS

    • Suspense reacts to suspending reads (`lazy`, `use` with a cached promise, Suspense-enabled frameworks), not to effects.
    • Promises must be cached outside render; creating one in render refetches forever.
    • Transitions prevent hiding already visible content behind a fallback.
    • Boundary placement is a UX decision: which parts load together.
    • Error boundaries handle rejected promises; Suspense handles pending ones.

    RED FLAGS

    • "Suspense makes any async code show a spinner."
    • Calling `use(fetch(url))` directly in a client component.
    • One boundary around the whole app, so any load blanks the page.

    LIKELY FOLLOW-UPS

    • How does streaming SSR use Suspense boundaries?
    • What is a request waterfall and how do Suspense boundaries create or avoid one?

    SOURCES

  10. Q10Senior

    What are React Server Components, how do they differ from client components, and what can cross the boundary between them?

    Reveal model answer

    Server Components render ahead of time on the server (at build or request time); their code and dependencies are never sent to the browser, they can be `async` and read data directly, but they cannot use state, effects or browser APIs. Client components are marked by a `'use client'` directive at the top of a module, which makes that module and everything it imports client code; they are still prerendered to HTML, then hydrated and interactive. A client module cannot import a Server Component, but a Server Component can render one and pass it to a client component as `children` or another prop. Props crossing the boundary must be serializable: primitives, plain objects and arrays, `Date`, `Map`, `Set`, promises, JSX, and Server Functions marked `'use server'`, but not ordinary functions or class instances.

    WHAT A STRONG ANSWER COVERS

    • Server Components ship no component JS; client components ship and hydrate.
    • `'use client'` marks a module boundary, and everything it imports becomes client code.
    • Compose by passing server-rendered JSX into client components as children.
    • Props across the boundary must be serializable; event handlers cannot be passed.
    • `'use server'` marks Server Functions, not Server Components; treat them as public endpoints that need auth checks.

    RED FLAGS

    • "Server Components are the same as SSR."
    • Putting `'use client'` at the root layout, making the whole app client code.
    • Leaking secrets by passing a full database record as props to a client component.

    LIKELY FOLLOW-UPS

    • How would you keep a server-only module from being imported into client code?
    • Where would you put a theme provider that needs state?

    SOURCES

  11. Q11Mid

    What does an error boundary catch, what does it miss, and how do you write one today?

    Reveal model answer

    An error boundary catches errors thrown while rendering its descendants, in their lifecycle methods and effects, and from rejected promises read with `use`, and shows fallback UI instead of unmounting the whole app. It does not catch errors in event handlers, in asynchronous code such as `setTimeout` (functions passed to `startTransition` are the exception, and their errors do reach the boundary), during server-side rendering, or in the boundary itself; handle those with try/catch, state and your own error reporting. As of React 19 there is still no hook equivalent, so a boundary is a class with `static getDerivedStateFromError` to render the fallback and `componentDidCatch` to log, or you use the `react-error-boundary` package. Place boundaries around independent regions such as a route or a widget, give the fallback a retry that resets it, and send the error to monitoring.

    WHAT A STRONG ANSWER COVERS

    • Catches render, lifecycle and effect errors in children, not in the boundary itself.
    • Misses event handlers, async callbacks (except transition functions) and SSR errors.
    • Class component (or `react-error-boundary`); no function-component API yet.
    • Granular placement keeps the rest of the UI usable; provide a reset or retry.

    RED FLAGS

    • One boundary at the root that shows a blank "Something went wrong" for any error.
    • Expecting a boundary to catch a failed `fetch` in an `onClick` handler.
    • Swallowing errors without logging them.

    LIKELY FOLLOW-UPS

    • How would you reset a boundary when the user navigates to another route?

    SOURCES

  12. Q12Mid

    Review this component: `function TodoList({ todos }) { const [items, setItems] = useState(todos); function Row({ t }) { return <li>{t.title}</li>; } const add = (t) => { items.push(t); setItems(items); }; return <ul>{items.map(t => <Row t={t} />)}</ul>; }`

    Reveal model answer

    There are four bugs. Copying `todos` into state means later prop changes are ignored, so either own the list here or derive it from props, not both. `Row` is defined inside `TodoList`, so it is a new component type on every render and React unmounts and remounts every row, losing state, focus and performance; move it to module scope. `add` mutates the array and passes the same reference back, so `Object.is` sees no change and React may skip the update; create a new array with `setItems(prev => [...prev, t])`. Finally the mapped rows have no `key`, so they should use a stable `t.id`.

    WHAT A STRONG ANSWER COVERS

    • Redundant state copied from props goes stale.
    • Components defined inside components remount every render.
    • Never mutate state; set a new reference, and prefer the updater form.
    • Lists need stable keys from the data.

    RED FLAGS

    • Spotting only the missing key.
    • Suggesting `useCallback` or `memo` as the fix for the nested component.
    • Fixing the mutation with `setItems(items.concat(t))` while keeping the stale prop copy unexplained.

    LIKELY FOLLOW-UPS

    • How would you change this if the parent must also know about added items?

    SOURCES