React Interview Questions

Common React interview questions covering the virtual DOM, hooks, context, performance, and testing.

A curated set of React interview questions, ordered roughly from fundamentals to more advanced — the kind you'll actually be asked in real screens and on-sites.

Core concepts

Q: What problem does the virtual DOM actually solve? Directly reading from and writing to the real browser DOM is comparatively expensive. React keeps an in-memory tree describing the UI, and on every state change it builds a new version of that tree and diffs it against the previous one, then applies only the minimal set of real DOM operations needed to reconcile the difference. The benefit isn't that the virtual DOM is "faster than the DOM" in some absolute sense — it's that it lets you write simple, declarative "re-render everything" code while React does the work of figuring out the efficient real update underneath.

Q: What's the difference between props and state? Props are data passed into a component from its parent — read-only from the receiving component's perspective, and owned by whoever passed them. State is data a component manages internally via useState (or useReducer), which the component itself can update, and which triggers a re-render when it changes. A common pattern is "lifting state up": state lives in a shared parent, and is passed down to children as props along with a setter function to update it.

Q: Why does React want a key prop on every item in a rendered list, and what goes wrong without one (or with the wrong one)? key gives React a stable identity for each item across re-renders, so it can match old list items to new ones correctly when the list is reordered, filtered, or has items added/removed in the middle. Without a good key (or using the array index as a key when the list can reorder), React can misattribute state to the wrong item — for example, a text input's typed value ending up attached to a different row after a reorder, because React matched items by position rather than identity. The fix is a key derived from stable, unique data (like a database ID), not the array index.

Hooks

Q: What's the most common useEffect bug, and how do you avoid it? Omitting a value from the dependency array that the effect's callback actually reads. The effect then keeps using a stale value captured from whichever render it last ran in — a classic case being a fetch effect that references a prop like userId but has [] as its dependency array, so it never re-fetches when userId changes. The fix is to include every reactive value the effect reads in the dependency array (the exhaustive-deps ESLint rule catches this automatically), and to restructure the effect if that produces more re-runs than intended, rather than suppressing the warning.

Q: Why can't the function passed to useEffect be async directly? useEffect's callback is expected to either return nothing or return a cleanup function. An async function always returns a Promise instead, which React would try (incorrectly) to treat as that cleanup function. The fix is to define an inner async function inside the effect and call it immediately, keeping the effect callback itself synchronous.

Q: What's the difference between useState and useRef for storing a value across renders? Both persist a value between re-renders, but updating state with useState's setter schedules and triggers a re-render, while updating a ref's .current does not — the component simply keeps the new value silently. Use useState for anything that should visibly affect what's rendered; use useRef for things like DOM node references, timer IDs, or any mutable value that needs to persist but shouldn't itself cause a re-render.

Forms

Q: What's the difference between a controlled and an uncontrolled input? A controlled input's value is driven entirely by React state — its value prop is set from state, and an onChange handler updates that state on every keystroke, making React the single source of truth. An uncontrolled input manages its own value internally in the DOM, and React only reads it on demand (typically via a ref), rather than tracking every keystroke in state. Controlled inputs make validation, conditional disabling, and formatting straightforward since the current value is always available in state; uncontrolled inputs involve less re-rendering and are simpler for cases like a plain file input or a form you only need to read once on submit.

State management and performance

Q: When is React Context enough, and when would you reach for a library like Redux or Zustand instead? Context plus useReducer covers sharing state across a moderate subtree without adding a dependency, but every component consuming a context re-renders whenever that context's value changes — there's no built-in way to subscribe to just one field inside it. That's fine for something like a theme or an auth user that changes rarely and is read by a modest number of components. A dedicated state library becomes worth its cost once state updates frequently and is read by many unrelated components, since it offers selector-based subscriptions (a component re-renders only when the specific slice it reads changes) and often ships DevTools/time-travel debugging that plain Context doesn't provide.

Q: What's the difference between what useMemo and useCallback each memoize? useMemo caches the result of a calculation, recomputing it only when a listed dependency changes — useful for an expensive derived value like a filtered or sorted list. useCallback caches a function reference itself, so a function passed as a prop doesn't look "new" on every render. They're closely related (useCallback(fn, deps) is essentially useMemo(() => fn, deps)), but solve different problems: useMemo avoids redoing expensive work, while useCallback avoids defeating a child's React.memo by handing it a fresh function reference every render.

Q: Why doesn't wrapping a component in React.memo help if you're still passing it a new object or array literal as a prop on every render? React.memo skips a re-render only when a shallow comparison of the new props against the previous ones finds no differences. An object or array literal created inline in the parent's render (<Child options={{ sort: "asc" }} />) is a brand-new reference every single render, even if its contents are identical — so the shallow comparison sees a "changed" prop every time and re-renders the child regardless of the memo wrapper. Fixing this requires memoizing the object itself with useMemo, or restructuring the prop into primitives memo can actually compare by value.

Testing and routing

Q: Why does React Testing Library encourage querying by role or label text instead of a CSS selector or a data-testid? Querying by role/label mirrors how a real user (or assistive technology) perceives the page, so a test built that way keeps passing through internal refactors — swapping useState for useReducer, renaming an internal variable — as long as the component's observable behavior doesn't change. A data-testid or CSS selector couples the test to implementation details invisible to an actual user, and is explicitly documented by the library's own team as a last resort rather than a default choice.

Q: In React Router, what's the difference between a Route's element and rendering child routes through <Outlet />? element is the component a Route renders when its own path matches the current URL. <Outlet />, placed inside a parent route's rendered element, is a placeholder marking where that parent's matched child route should render — it's what makes nested routing work: a shared layout (navigation, a sidebar) stays mounted across navigations between its children, while only the <Outlet /> content swaps out, rather than the whole layout re-rendering from scratch on every route change.