· career · 9 min read
Top 10 React Interview Questions That Will Stump Even Experienced Engineers
Prepare for the hardest React interview questions FAANG teams ask. This guide breaks down 10 challenging questions, what interviewers are testing, model answers, common pitfalls, and follow-ups to demonstrate deep expertise.

Introduction
If you’ve been coding React for years, you probably know how to build components, wire up state, and write tests. But FAANG interviewers often focus on the edges - the tricky details, trade-offs, and surprising pitfalls that reveal whether a candidate fully understands React’s runtime, reconciliation, hooks model, and performance characteristics.
Below are 10 advanced React interview questions that commonly trip up senior engineers. For each question you’ll find: what the interviewer is testing, a succinct model answer, common misconceptions, and follow-up topics to demonstrate deeper mastery.
1) How does reconciliation work and why do keys matter?
What they’re testing: knowledge of React’s reconciliation algorithm, how keys are used to match elements across renders, and the practical effects on component identity and performance.
Model answer:
- React compares the previous and next virtual DOM trees and tries to minimize DOM operations.
- At the same level in a list, React uses the
key
prop to match elements between renders. When keys change or are unstable (like using array index carelessly), React may reuse the wrong DOM node or unmount/remount components. - Stable, unique keys (e.g., database IDs) preserve component state and reduce unnecessary mounts.
Common pitfalls:
- Using array index as a key for reorderable lists or lists that change length. This causes UI bugs and loss of state in child components.
- Thinking keys affect ordering or that keys are required everywhere (they’re only required for siblings in lists).
Example:
// Bad: index used as key
items.map((item, i) => <Todo key={i} item={item} />);
// Better: stable id
items.map(item => <Todo key={item.id} item={item} />);
Follow-ups to show depth:
- Explain how reconciliation differs for different element types (text vs component vs DOM node).
- Describe how
React.memo
orshouldComponentUpdate
interacts with reconciliation.
References: https://reactjs.org/docs/reconciliation.html
2) Virtual DOM vs real DOM and the diffing algorithm
What they’re testing: whether you understand why React uses a virtual DOM and the performance trade-offs of diffing.
Model answer:
- The virtual DOM is an in-memory representation of UI that lets React compute minimal changes before touching the real DOM, which is expensive.
- React uses heuristics and assumptions (e.g., elements of different types are replaced, same type may be updated) to make diffing O(n) instead of O(n^2).
Common misconceptions:
- Virtual DOM always makes things faster - not necessarily. For tiny UIs the overhead may be unnecessary; for complex apps it avoids costly DOM thrashing.
Follow-ups:
- When might manual DOM manipulation be appropriate? When integrating third-party libraries (D3) or for very specific micro-optimizations.
- How Fiber changed reconciliation and introduced incremental rendering.
Reference: https://reactjs.org/docs/faq-internals.html
3) Explain the Rules of Hooks and common pitfalls (stale closures, dependencies)
What they’re testing: deep knowledge of hooks, execution model, closures, and how useEffect
dependency arrays work.
Model answer:
- Hooks must be called at the top level of React function components or custom hooks and in the same order on every render.
- Effects capture values from the render in which they were defined. If dependencies are omitted or incorrect, an effect can use stale data.
- Always declare dependencies or justify when you intentionally omit them (and use refs or
useCallback
to stabilize values).
Common pitfalls:
- Omitting dependencies from
useEffect
and relying on stale variables. - Creating new functions/objects inline on every render and then memoizing them incorrectly.
Example - stale closure bug:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// This closure sees the count value from the initial render
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // [] causes stale closure
}
// Fix: use functional update
setCount(c => c + 1);
Follow-ups:
- When to use
useRef
vs state? (Mutable container that doesn’t trigger re-renders vs state that does.) - When to use
useCallback
anduseMemo
- and why overuse can be harmful.
References: https://reactjs.org/docs/hooks-rules.html, https://reactjs.org/docs/hooks-faq.html
4) Concurrent rendering, Suspense, and startTransition
What they’re testing: familiarity with React 18+ features, scheduling priorities, and how to build resilient UIs.
Model answer:
- Concurrent rendering allows React to interrupt renders and prioritize more urgent updates (like typing) over less urgent ones (like loading a large list).
startTransition
anduseTransition
mark state updates as non-urgent so React can keep the UI responsive.Suspense
lets you show a fallback while waiting for data (or code) to arrive; with concurrent features it becomes more powerful (avoid jank during loading).
Common pitfalls:
- Expecting
startTransition
to magically make everything fast - it’s a tool for prioritization, not a performance bandage. - Misunderstanding that concurrent mode changed the semantics of components - it affects timing and ordering but should preserve correctness if code is pure.
Example:
import { startTransition } from 'react';
function onSearchChange(nextValue) {
setQuery(nextValue);
startTransition(() => {
setResults(heavyFilter(nextValue));
});
}
Follow-ups:
- Describe how Suspense for data fetching works with frameworks like Relay or React Query.
- Explain how to test concurrent behavior and debug contentions.
Reference: https://reactjs.org/docs/concurrent-mode-intro.html
5) Context API: when to use it and performance concerns
What they’re testing: whether you know that Context is for sharing values, but that updates re-render consumers and can cause performance issues.
Model answer:
- Context is great for global-ish data like locale, theme, or auth, but putting frequently-changing values in context can cause many re-renders.
- Techniques to mitigate: split contexts (one per logical concern), memoize provider value (
useMemo
), store stable callbacks (useCallback
), or keep high-frequency state local and expose mutation functions via context.
Common pitfalls:
- Passing a new object literal as the provider value every render, unintentionally forcing consumers to update.
- Using context as a substitute for good state boundaries or for very frequent updates (e.g., cursor position).
Example:
// Bad: forces re-renders
<AuthContext.Provider value={{ user, logout }}>...</AuthContext.Provider>;
// Better: memoize
const providerValue = useMemo(() => ({ user, logout }), [user, logout]);
Follow-ups:
- Compare Context with external state libraries (Redux, Zustand) and when to prefer one.
Reference: https://reactjs.org/docs/context.html
6) State management: useState vs useReducer vs external stores
What they’re testing: ability to choose the right tool for predictable updates and complex state transitions.
Model answer:
useState
is ideal for simple, local state.useReducer
helps with complex state transitions or when the next state depends on the previous state and you want a predictable reducer pattern.- External stores (Redux, MobX, Zustand) provide global state with their own trade-offs: predictability, debugging tooling, or simplicity.
Common pitfalls:
- Overusing
useReducer
for trivial state or using external global state when component-local state would suffice. - Mutating state directly instead of returning new objects (breaking pure update semantics).
Example:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
Follow-ups:
- Explain how to implement optimistic updates or rollbacks in reducers.
- Discuss normalizing relational data and how it affects state shape.
7) SSR, hydration mismatches, and streaming
What they’re testing: knowledge of server-side rendering, hydration pitfalls, and trade-offs between SSR and client rendering.
Model answer:
- SSR renders HTML on the server to improve time-to-first-byte and SEO; the client then hydrates the server-rendered markup to attach event handlers.
- Hydration mismatches occur when server and client render different outputs (e.g., code that depends on
window
, current time, random values, or non-deterministic rendering). These cause warnings or client-side re-rendering. - Streaming SSR (in React 18+) can progressively send HTML to the client for faster perceived load.
Common pitfalls:
- Using
Math.random()
orDate.now()
during render without guarding, causing hydration mismatch. - Accessing browser-only globals (localStorage, window) during server render.
Mitigations:
- Use
useEffect
for browser-only operations. - Make server render deterministic; pass initial data from server to client.
Reference: https://reactjs.org/docs/react-dom-server.html
8) Performance optimizations: memo, useMemo, useCallback - when and when not to use
What they’re testing: whether you understand the overhead vs benefit of memoization and when these tools are appropriate.
Model answer:
React.memo
memoizes a component render based on props;useMemo
anduseCallback
memoize values and functions between renders.- They are useful when (a) re-rendering is expensive or (b) you need referential equality to prevent child re-renders.
- Overusing them adds cognitive complexity and runtime overhead; measure with profiling before optimizing.
Common pitfalls:
- Blindly wrapping everything with
React.memo
oruseCallback
without verifying a performance problem. - Relying on
useMemo
to prevent side effects - it is a performance optimization, not a semantic guarantee.
Example:
// Prevent child re-render by stabilizing callback
const onClick = useCallback(() => setOpen(s => !s), [setOpen]);
<ExpensiveChild onClick={onClick} />;
Follow-ups:
- How to use the profiler in React DevTools to find costly renders.
- How to avoid prop drilling alternatives (context, composition, selectors).
Reference: https://reactjs.org/docs/hooks-reference.html#usememo
9) React events and synthetic event pooling (historical nuance)
What they’re testing: knowledge of the event system, lifecycle and a historical detail that can reveal depth.
Model answer:
- React uses a synthetic event system that normalizes browser differences and historically pooled events for performance.
- As of React 17+, event delegation changed (React attaches listeners to the root), and pooling has been removed in React 17+ for simpler semantics.
Common pitfalls:
- Relying on accessing event properties asynchronously without calling
event.persist()
(historically needed when events were pooled). In modern React this is mostly unnecessary, but you should still be careful when reading event properties inside async callbacks.
Follow-ups:
- Explain the difference between native DOM listeners and React synthetic events and when to use one over the other.
Reference: https://reactjs.org/docs/events.html
10) Testing strategy and pitfalls (unit, integration, e2e) for React apps
What they’re testing: practical knowledge of testing tools and trade-offs, and the ability to test UIs effectively without fragile tests.
Model answer:
- Use React Testing Library (RTL) for component-level tests focused on behavior and user interactions rather than implementation details. Avoid asserting on internal state or component internals.
- Snapshot tests are useful for detecting regressions, but they should be kept small and maintained deliberately - large snapshot files are brittle.
- Use e2e tests (Cypress, Playwright) for flows, and mock network requests appropriately.
Common pitfalls:
- Overreliance on Enzyme shallow rendering leading to tests coupled to implementation.
- Tests that assert on DOM class names or structure that change frequently.
Example - user-centric test with RTL:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('submits form', async () => {
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'a@b.com');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText(/welcome/i)).toBeInTheDocument();
});
Follow-ups:
- How to test Suspense and async components.
- Strategies for flaky tests and CI integration.
Reference: https://testing-library.com/docs/react-testing-library/intro
Conclusion: how to prepare for FAANG-level React interviews
- Focus on fundamentals and trade-offs: interviewers are looking for engineers who can reason about performance, correctness, and developer ergonomics - not just who can memorize APIs.
- Practice explaining why a solution works and what trade-offs it has. Bring up complexity, edge cases, and measurable outcomes.
- Use small whiteboard or coding exercises to demonstrate: identify the invariant, show an example bug (e.g., stale closure), and explain a fix.
- Keep up with recent React releases (Concurrent features, React 18 APIs) and know which features are stable vs experimental.
Further reading and resources
- React docs: Hooks, Concurrent Mode, Reconciliation - https://reactjs.org/
- React Testing Library - https://testing-library.com/
- React blog for RFCs and new features - https://reactjs.org/blog/
Good luck - and remember: the best answers combine correctness, reasoning about trade-offs, and concrete examples that demonstrate your understanding.