Master the 20 most common React frontend interview questions and detailed answers on DeepFrontend. Covers Virtual DOM reconciliation, Hooks lifecycle, stale closures, re-rendering triggers, Context API, and modern React 19 patterns.
Preparing for a Mid-Level (L4 / Mid-Senior) Frontend Interview?
If you have 1 to 4 years of experience with React, interviewers aren't testing you on whether you can write a basic onClick handler. They want to know:
Do you understand how React actually executes and renders?
Can you avoid common performance traps like unnecessary re-renders and stale closures?
Do you write clean, predictable state and component architectures?
Here are the 20 most frequently asked React interview questions, broken down with concise mental models, clear code examples, and the specific traps interviewers look for.
Q1: What is the Virtual DOM, and how does React's Reconciliation process work?#
The 30-Second Answer:
The Virtual DOM (VDOM) is a lightweight JavaScript object representation of the real DOM tree kept in browser memory. When state changes, React builds a new Virtual DOM tree, compares it against the previous tree using a diffing heuristic called Reconciliation, and calculates the minimum batch of changes needed before updating the real DOM.
The Detailed Breakdown:
Directly manipulating the real browser DOM (via document.createElement or innerHTML) is fast, but triggering subsequent browser layout recalculation (reflow) and repaints across large subtrees is slow. React minimizes layout thrashing by:
Render Phase: Comparing the old VDOM against the new VDOM to find differences (O(n) heuristic algorithm).
Commit Phase: Batch-applying only the changed DOM attributes and nodes in a single paint operation.
Interviewer Tip: Emphasize that in modern React (React 18 & 19), the Virtual DOM is implemented as a Fiber architecture, which allows React to break rendering work into interruptible chunks rather than blocking the browser main thread.
Q2: What is the fundamental difference between State and Props in React?#
The 30-Second Answer:
Props are external inputs passed into a component from its parent (like function arguments). They are read-only and immutable from the perspective of the child component.
State is internal data managed and owned by the component itself (via useState or useReducer). It changes over time in response to user actions or network events.
Characteristic
Props
State
Origin
Passed from parent component
Managed internally within component
Mutability
Read-only (Immutable)
Mutable via state updater functions
Trigger Re-render?
Yes, if the parent passes new values
Yes, calling setState triggers re-render
Usage
interface UserCardProps { name: string; // Prop: given by parent}export function UserCard({ name }: UserCardProps) { const [isFollowing, setIsFollowing] = useState(false); // State: local to this card return ( <div> <h3>{name}</h3> <button onClick={() => setIsFollowing((prev) => !prev)}> {
Q3: Why do list items require a key prop, and why is using an array index dangerous?#
The 30-Second Answer:
React uses the key prop during reconciliation to identify which items in a list have been added, moved, updated, or removed. A stable key prevents React from recreating existing DOM nodes.
The Danger of Index as Key:
If items in a list are re-ordered, sorted, prepended, or deleted, using the array index (key={index}) confuses React's diffing engine:
If you prepend an item to the top of an array, the new item gets index 0.
React thinks index 0 merely changed its props rather than being a newly inserted element.
This causes subtle state bugs where uncontrolled inputs, checkboxes, or animation states remain bound to the wrong DOM node!
// ❌ BAD: Index as key breaks input state when items are sorted or prepended{items.map((item, index) => ( <TodoRow key={index} item={item} />))}// ✅ GOOD: Stable, unique business identifier{items.map((item) => ( <TodoRow key={item.id} item={item} />))}
Rule of Thumb: Only use index as a key if the list is strictly static (never reordered, filtered, or mutated). Otherwise, always use a unique ID (e.g. database ID or generated UUID).
Q4: What is the difference between Controlled and Uncontrolled Components?#
The 30-Second Answer:
Controlled Component: Form input data is handled directly by React state. The component's value is set via value={state} and updated via onChange. React is the "single source of truth".
Uncontrolled Component: Form input data is stored directly in the browser DOM. Values are pulled on demand using a ref (e.g. inputRef.current.value).
// 1. Controlled: React holds the stateexport function ControlledForm() { const [email, setEmail] = useState(""); return <input value={email} onChange={(e) => setEmail(e.target.value)} />;}// 2. Uncontrolled: DOM holds the state, read via refexport function UncontrolledForm() { const emailRef = useRef<HTMLInputElement>(null); const handleSubmit = (e: React.FormEvent) =>
When to use which:
Use Controlled when you need instant inline validation, dynamic disabling of submit buttons, or conditional formatting.
Use Uncontrolled (or libraries like React Hook Form) for massive forms to avoid re-rendering the whole form on every single keystroke.
Q5: What are the Rules of Hooks, and why do they exist?#
The 30-Second Answer:
Only call hooks at the top level: Do not call hooks inside loops, conditions, or nested functions.
Only call hooks from React function components or custom hooks.
Why does this rule exist?
React does not identify hooks by property names. Internally, React stores all hooks for a component as a singly linked list ordered by their execution call sequence:
If a hook is placed inside an if (condition) statement that skips execution on a subsequent render, the internal pointer shifts out of sync. Hook 3 will read the state of Hook 2, causing silent state corruption or crashes!
Q6: How does useEffect work, and what is the purpose of the Cleanup function?#
The 30-Second Answer:useEffect allows you to synchronize your component with an external system (APIs, DOM events, timers). It runs after the browser has painted the screen.
The Lifecycle of useEffect:
No Dependency Array (useEffect(fn)): Runs after every render.
Empty Dependency Array (useEffect(fn, [])): Runs once after initial mount.
With Dependencies (useEffect(fn, [id])): Runs on mount and whenever any dependency value changes by reference (Object.is).
The Cleanup Function:
A function returned by the effect callback. It runs:
Before the effect re-runs with new values (clearing previous subscriptions).
Q7: What is a "Stale Closure" in React Hooks, and how do you fix it?#
The 30-Second Answer:
In JavaScript, a closure captures variables from its lexical scope at the time the function is created. If a React callback (such as setInterval or setTimeout inside useEffect) captures state from an earlier render without listing it in the dependency array, it continues referencing the old ("stale") value indefinitely.
// ❌ BUG: Stale Closureexport function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { // 'count' is captured as 0 when mounted. It will always set 0 + 1 = 1! setCount(count + 1); }, 1000); return () => clearInterval(timer); }, []); // Missing count dependency return <div>{count}</div>;}
The Two Solutions:
Use Functional State Updates (Best practice):
// ✅ Passed updater function receives latest state from ReactsetCount((prev) => prev + 1);
Include the dependency in the array (or use a ref to track the mutable value if re-subscribing is costly).
Q8: What is the difference between useMemo and useCallback, and when should you avoid them?#
The 30-Second Answer:
useMemo caches the result of a calculation: const memoizedValue = useMemo(() => computeHeavy(a, b), [a, b]);
useCallback caches the function definition itself between renders: const memoizedFn = useCallback(() => doSomething(a), [a]);
(In fact, useCallback(fn, deps) is just syntactic sugar for useMemo(() => fn, deps).)
// useMemo caches an expensive transformed listconst filteredProducts = useMemo(() => { return products.filter((p) => p.category === activeCategory);}, [products, activeCategory]);// useCallback preserves function reference so child memoization does not breakconst handleSelect = useCallback((id: string) => { setSelectedId(id);}, []);
When NOT to use them:
Do not wrap every function or simple calculation in useCallback / useMemo!
Creating closures and dependency array comparisons has its own CPU and memory overhead.
For cheap operations like filtering 10 items or basic arithmetic, the overhead of the hook is higher than recalculating.
Only use them when:
Passing callbacks to a child component wrapped in React.memo.
The function is a dependency in another hook's dependency array (useEffect).
The calculation is verifiably expensive (e.g. processing 10,000 items).
Q9: What is the difference between useRef and useState?#
The 30-Second Answer:
Both persist values across component re-renders.
useState triggers a re-render when the state is updated.
useRef does NOT trigger a re-render when ref.current is mutated.
Q10: What triggers a component to re-render in React?#
The 30-Second Answer:
A React component re-renders if and only if:
Its own state changes (via useState or useReducer dispatch).
Its parent component re-renders (by default, all children re-render regardless of whether their props changed).
A Context value it consumes changes (via useContext).
Common Misconception:"A component re-renders when its props change."Correction: If a parent re-renders, the child re-renders even if its props did not change at all, unless the child is explicitly wrapped in React.memo!
Q11: How does the Context API work, and what is the "re-render all consumers" problem?#
The 30-Second Answer:
React Context provides a way to pass data deeply down a component tree without manually passing props at every level ("prop drilling").
The Performance Problem:
Whenever the value passed to a <Context.Provider value={value}> changes by reference, every component that calls useContext(MyContext) will re-render, even if that component only cares about a tiny property of the context that didn't change!
// ❌ POTENTIAL ISSUE: Any change to theme re-renders components that only need userconst AppContext = createContext<{ user: User; theme: string } | null>(null);
How to solve it:
Split Contexts: Separate state by domain (e.g. UserContext vs. ThemeContext).
Memoize the Provider value: Ensure the value object has a stable reference:
Use specialized state libraries (like Zustand or Jotai) which support fine-grained selector subscriptions.
Q12: What is "Prop Drilling", and what are 3 clean ways to avoid it?#
The 30-Second Answer:Prop drilling occurs when you pass props through multiple intermediate components that do not need the data themselves, solely to reach a deeply nested child component.
3 Clean Solutions:
Component Composition (Often overlooked!): Instead of passing props down, pass the fully-formed child component down via children:
React Context API: Ideal for global or feature-level state like authentication, themes, or localization.
External State Management: Use stores with selector hooks (like Zustand, Redux Toolkit, or Jotai) where any component can directly subscribe to state slices.
Q13: How does React.memo work, and why does passing inline functions or objects break it?#
The 30-Second Answer:React.memo is a Higher-Order Component that skips re-rendering a component if its new props are shallowly equal to its previous props (Object.is equality check on each prop).
Why inline objects and functions break memoization:
In JavaScript, object and function literals create a brand-new memory reference on every render:
// ❌ BUG: Broken memoization!export function Parent() { return ( // 'style' and 'onClick' create NEW object/function references on EVERY render! <ExpensiveList style={{ padding: 12 }} onItemClick={(id) => console.log(id)} /> );}
Even though ExpensiveList is wrapped in React.memo, React checks prevProps.onItemClick === nextProps.onItemClick. Because the references differ, React.memo assumes props changed and re-renders anyway!
The Fix: Wrap the function in useCallback and memoize or hoist the object with useMemo or outside component scope.
Part 4: Performance, Data Fetching & Side Effects#
Q14: How should you fetch data in modern React (useEffect vs. TanStack Query)?#
The 30-Second Answer:
While fetching data inside useEffect with fetch() is fine for trivial prototypes, modern production applications use dedicated server-state libraries like TanStack Query (React Query) or SWR (or React 19 / Next.js Server Components).
Why TanStack Query is superior for interviews:
Mention the 5 problems it solves out of the box:
Deduplication of identical requests across components.
Background caching and optimistic updates.
Automatic refetch on window focus or network reconnect.
Pagination and infinite scroll helpers.
Elimination of race conditions and memory leak cleanup boilerplate.
Q15: What are Error Boundaries in React, and what errors do they NOT catch?#
The 30-Second Answer:
An Error Boundary is a React component that catches JavaScript errors anywhere in its child component tree, logs the error, and displays a graceful fallback UI instead of crashing the entire application.
In React, Error Boundaries must be class components using getDerivedStateFromError and componentDidCatch (or using the popular react-error-boundary package).
What Error Boundaries DO NOT catch (Critical interview trap!):
Event handlers (e.g. onClick errors — use standard try/catch here).
Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks).
Server-Side Rendering (SSR) errors.
Errors thrown inside the Error Boundary itself (rather than in its children).
class ErrorBoundary extends React.Component<Props, { hasError: boolean }> { state = { hasError: false }; static getDerivedStateFromError(error: Error) { return { hasError: true }; // Updates state so the next render shows fallback } componentDidCatch(error: Error, info: React.ErrorInfo) { logErrorToService(error, info); } render() { if (this.state.hasError) { return <h2>Something went wrong. Please reload.</
Q16: What is Code Splitting and how do React.lazy and <Suspense> work?#
The 30-Second Answer:
By default, Webpack or Vite bundles all your application code into a single large JavaScript file. Code Splitting splits this bundle into smaller chunks that are loaded on demand when the user actually navigates to that feature, drastically improving initial page load time.
import React, { Suspense, lazy } from "react";// AdminPanel bundle will NOT be downloaded until this component renders!const AdminPanel = lazy(() => import("./AdminPanel"));export function App() { return ( <div> <Navbar /> <Suspense fallback={<div className="spinner">Loading Admin Panel...</div>}> <AdminPanel /> </Suspense> </div> );}
React.lazy takes a dynamic import() statement and returns a React component promise.
<Suspense> specifies a fallback UI (like a skeleton loader or spinner) while the bundle chunk is being fetched over the network.
Q17: What is the difference between useEffect and useLayoutEffect?#
The 30-Second Answer:
useEffect runs asynchronously AFTER the browser paints the screen. It does not block user visual updates. (Use for 98% of cases: API calls, event listeners).
useLayoutEffect runs synchronously BEFORE the browser paints the screen, immediately after DOM mutations. (Use for measuring DOM nodes like tooltips, popovers, or scroll positions to avoid visual flickering).
// Use useLayoutEffect to measure element dimensions before the user sees ituseLayoutEffect(() => { const { height } = tooltipRef.current.getBoundingClientRect(); setOffset(height + 8); // Updated BEFORE paint, so no visual jumping!}, []);
Q18: What are Custom Hooks, and how do you build one?#
The 30-Second Answer:
A Custom Hook is a JavaScript function whose name starts with "use" and which can call other React hooks. Custom hooks allow you to extract and share stateful logic between components without duplicating code or changing the component hierarchy.
Example: A Production useDebounce Hook
import { useState, useEffect } from "react";export function useDebounce<T>(value: T, delayMs: number = 300): T { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delayMs); // Cleanup: Resets timer if value changes before delay finishes return () => clearTimeout(handler); }, [value, delayMs]);
Q19: What is the difference between React Server Components (RSC) and Client Components?#
The 30-Second Answer:
In modern React (React 19 / Next.js App Router):
Server Components (Default): Execute only on the server. They never send JavaScript to the client browser, can query databases directly, and have zero impact on bundle size. They cannot use browser APIs or state hooks (useState, useEffect).
Client Components ("use client"): Pre-rendered on the server and hydrated in the browser. They have access to browser APIs, user event listeners (onClick), and React state hooks.
Feature
Server Components (RSC)
Client Components ("use client")
Execution Environment
Server only
Server (initial HTML) + Browser
Client Bundle Size
0 KB
Includes component JS
Can use useState / useEffect?
❌ No
✅ Yes
Q20: What is Automatic Batching in modern React (React 18 & 19)?#
The 30-Second Answer:Batching is when React groups multiple state update calls into a single re-render pass to optimize performance.
In React 17 and earlier: React only batched updates inside native React event handlers (like onClick). Updates inside setTimeout, Promise.then, or fetch callbacks were NOT batched and triggered separate re-renders.
In React 18+ (Automatic Batching): React batches all state updates automatically, regardless of whether they happen in event handlers, promises, timeouts, or native events.
function handleFetch() { fetch("/api/user").then(() => { // In React 17: Triggered 2 separate re-renders! // In React 18+: Automatically batched into 1 re-render! setIsLoading(false); setUser(data); });}
(If you ever need to opt out of batching for an urgent DOM update, you can use flushSync(() => setState(...)).)
Want to master these concepts with interactive live code playgrounds, visual execution simulators, and active-recall quizzes? DeepFrontend provides 315+ in-depth topics across 25 domains.
Below are direct links to explore every related domain and topic available on the platform: