ReactInterview Questions

React Interview Questions

React interviews test conceptual understanding, practical knowledge, and architectural thinking. This collection covers 25+ questions organized by level — from fundamentals to advanced internals and system design. Study each answer until you can explain it in your own words, not recite it.

---

Beginner

What is React?

React is a JavaScript library for building user interfaces, developed by Meta. It uses a component-based model where UIs are built from small, reusable pieces. React maintains a virtual DOM and updates the real DOM efficiently using a process called reconciliation. It is declarative — you describe what the UI should look like for a given state, and React figures out how to make it so. React is intentionally minimal. It handles the view layer only. Routing, data fetching, and state management are handled by the ecosystem (React Router, TanStack Query, Zustand, etc.).

What is JSX?

JSX is a syntax extension for JavaScript that looks like HTML. It allows you to write markup directly inside JavaScript files. Browsers cannot execute JSX — it is compiled by Babel or TypeScript into React.createElement() calls (or _jsx() with the new transform).

    JSX is not HTML. Key differences: `class` becomes `className`,
    `for` becomes `htmlFor`, all tags must be closed, and JavaScript
    expressions are embedded with `{curly braces}`. JSX produces a React
    element — a plain JavaScript object describing what to render.

TSX
// JSX
const el = <h1 className="title">Hello, {name}!</h1>

// Compiles to:
const el = React.createElement('h1', { className: 'title' }, 'Hello, ', name, '!')
What is the difference between state and props?

Props are inputs passed from a parent component to a child. They are read-only — the child cannot modify them. Props are how data flows down the tree. State is data owned and managed by the component itself. State can change over time (via setState / setX), and each change triggers a re-render. State is private to the component that declares it. A simple mental model: props are like function arguments, state is like a function's local variables.

What is the Virtual DOM?

The virtual DOM is an in-memory JavaScript representation of the real DOM. When state changes, React creates a new virtual DOM snapshot and diffs it against the previous one (reconciliation). Only the differences are applied to the real DOM. Direct DOM manipulation is slow because the browser recalculates layout and repaints. Batching changes via a virtual DOM and applying them in one pass reduces those expensive operations. Modern React (Fiber) can also interrupt and prioritize this work, keeping the UI responsive during heavy renders.

Why do we need keys in lists?

When React renders a list, it uses the key prop to uniquely identify each element across renders. Keys help React determine which items have been added, removed, or reordered, so it can make targeted DOM updates rather than re-rendering the entire list. Without correct keys, React re-creates DOM nodes unnecessarily, which breaks animations, resets input focus, and loses component state. Keys must be stable (not change between renders) and unique among siblings. Array index is a poor key because it changes when items are added or removed.

---

Intermediate

How does useEffect work?

useEffect lets you run side effects after React has rendered. It accepts a function and a dependency array. The function runs after every render where the dependencies changed. If the dependency array is empty ([]), the effect runs once after the first render (mount only). The function can return a cleanup function that runs before the next effect execution and when the component unmounts. This is how you remove event listeners, cancel fetch requests, and clear timers.

TSX
useEffect(() => {
  const controller = new AbortController()
  fetchData({ signal: controller.signal }).then(setData)
  return () => controller.abort()   // cleanup
}, [userId])                         // re-run when userId changes
What is the dependency array in useEffect?

The second argument to useEffect is the dependency array. It tells React which values the effect depends on: - No array — effect runs after every render - Empty array [] — effect runs once after mount - [a, b] — effect runs when a or b changes Every reactive value used inside the effect (state, props, functions) must be listed. The eslint-plugin-react-hooks exhaustive-deps rule enforces this automatically. Omitting a dependency does not make the effect run less — it creates a stale closure bug.

What is reconciliation?

Reconciliation is the algorithm React uses to diff the old virtual DOM tree against the new one and compute the minimum set of DOM changes required. React's diffing heuristics: (1) Elements of different types produce entirely different trees — React unmounts the old and mounts the new. (2) Elements of the same type update only their changed attributes. (3) The key prop tells React which child elements in a list correspond to which elements from the previous render. React does not do full tree diffing (O(n³)) — it uses these heuristics to achieve O(n) performance.

When would you use useCallback vs useMemo?

useCallback(fn, deps) memoizes a function. It returns the same function reference as long as dependencies have not changed. Use it when passing a callback to a memoized child component, or when a function is listed as a useEffect dependency. useMemo(fn, deps) memoizes a computed value. The factory function runs once and returns a cached value until deps change. Use it for expensive computations (filtering/sorting large arrays, computing derived state). Both should be applied only after profiling reveals a measurable problem. Premature memoization adds complexity for no benefit.

What is a controlled component?

A form element where React state is the single source of truth for the element's value. The component renders the current state value and updates state on every change via an event handler. React fully controls the form; the DOM reflects the state. The alternative is an uncontrolled component, where the DOM manages its own state and you access it via a ref. Controlled components enable validation on every keystroke, computed fields, and conditional disabling.

What are the Rules of Hooks?

There are two rules: 1. Only call hooks at the top level — never inside loops, conditions, or nested functions. This ensures hooks are called in the same order on every render, which is how React associates hook state with the correct call. 2. Only call hooks from React functions — either function components or custom hooks. Regular JavaScript functions cannot use hooks. The eslint-plugin-react-hooks/rules-of-hooks ESLint rule enforces both automatically.

What is lifting state up?

When two components need to share the same state, move the state to their closest common ancestor. The ancestor manages the state and passes it down as props, along with updater functions as callback props. This is the fundamental React pattern for sibling communication — there is no direct sibling-to-sibling data flow in React. State always flows down, events flow up.

Explain the Context API.

Context provides a way to pass data through the component tree without prop drilling. You create a context with createContext(defaultValue), wrap a subtree with Context.Provider value={...}, and read the value in any descendant with useContext(Context).

    Context is not a replacement for all prop passing — it adds a hidden
    dependency that makes components harder to reuse in isolation. Use it for
    genuinely global data: current user, theme, locale, and feature flags.
    For complex shared state, use a dedicated state manager.
What is React.memo?

React.memo is a higher-order component that wraps a component and memoizes its rendered output. On re-render, React compares the new props to the old props via shallow equality. If they are the same, React skips re-rendering the component and reuses the previous result. It is useful when a parent re-renders frequently but a child's props rarely change. Be aware that React.memo only does shallow comparison — new object or array references in props bypass the memoization.

How do you handle errors in React?

Error Boundaries are class components that implement componentDidCatch and getDerivedStateFromError. They catch JavaScript errors in their child component tree and render a fallback UI instead of crashing. The popular react-error-boundary library provides a functional wrapper. Error Boundaries do not catch errors in event handlers (use try/catch), async code (use .catch()), or server-side rendering.

TSX
import { ErrorBoundary } from 'react-error-boundary'

<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <ProfilePage />
</ErrorBoundary>

---

Advanced

How does React Fiber work?

React Fiber is the complete rewrite of React's reconciler introduced in React 16. It represents the component tree as a linked list of lightweight objects called "fibers." Each fiber tracks a unit of work to be done. The key innovation is that Fiber work is interruptible. React can pause reconciliation mid-tree (between fiber nodes), yield to the browser to handle user input or animations, and then resume. This enables Concurrent Mode features like useTransition and useDeferredValue, which let urgent updates (typing) preempt non-urgent ones (search results rendering).

What are React Server Components?

React Server Components (RSC) render on the server and send a serialized component tree to the client — not HTML, but a format React can reconstruct on the client without re-executing the server code. Server Components can directly access databases and secrets; they never ship their source code to the browser. Server Components cannot use state, effects, or event handlers. Client Components (marked "use client") handle interactivity. The two can be freely composed: a Server Component can render a Client Component as a child.

What is concurrent rendering?

Concurrent rendering is React 18's ability to prepare multiple versions of the UI simultaneously and interrupt low-priority work to handle urgent updates. Before concurrency, renders were synchronous and blocking — once started, nothing could interrupt them. APIs that leverage concurrent rendering: useTransition marks a state update as non-urgent, useDeferredValue defers a value until the browser has idle time, and Suspense integrates with the scheduler to show fallbacks without blocking.

How does React's reconciliation diffing algorithm work?

React's diffing uses three heuristics to achieve O(n) complexity: 1. Type heuristic — if a node's type changes (e.g., div to span, or ComponentA to ComponentB), React tears down the entire subtree and builds a new one. No further diffing is done under that node. 2. Props diffing — if the type is the same, React updates only the changed props/attributes on the existing DOM node. 3. Key heuristic — for lists, React uses the key prop to match children between renders. Keys allow React to detect reordering and move existing DOM nodes rather than re-creating them.

What is a stale closure in useEffect and how do you fix it?

A stale closure occurs when a useEffect callback closes over a variable (like count) at the time the effect was created, and that variable later changes. The effect still sees the old value because it captured the variable from a previous render's scope. Fix 1 — use a functional state update that receives the current value: setCount(c => c + 1) instead of setCount(count + 1). Fix 2 — add the variable to the dependency array. The effect re-runs with the fresh value, and the cleanup clears the previous subscription. The exhaustive-deps ESLint rule detects stale closure bugs automatically.

What is the difference between useEffect and useLayoutEffect?

Both run after render, but at different times: useEffect fires asynchronously after the browser has painted. This is the correct choice for the vast majority of effects: data fetching, subscriptions, event listeners. The user sees the new UI before the effect runs. useLayoutEffect fires synchronously after DOM mutations but before the browser paints. Use it when you need to read or write DOM measurements before the user sees the frame — for example, computing tooltip positions or synchronously animating layout changes. Misusing it blocks painting and degrades performance.

How would you optimize a slow React application?

Optimization should always be measurement-first. Use React DevTools Profiler to identify slow renders before applying any fix. Common fixes:

  • Wrap expensive child components in React.memo to skip re-renders when props are unchanged

  • Use useMemo for expensive computed values (filtering/sorting large datasets)

  • Use useCallback for stable function references passed to memoized children

  • Lazy-load heavy components and routes with React.lazy + Suspense

  • Virtualize long lists with react-virtual or @tanstack/virtual — render only visible rows

  • Move state down (colocate) to reduce how many components re-render per state change

  • Use useTransition to mark non-urgent updates so typing stays responsive during expensive renders

  • Avoid new object/array literals in JSX props — they defeat memoization

---

Architecture

How do you structure a large React application?

Organize by feature, not by technical type. Each feature (auth, checkout, dashboard) is a self-contained folder with its own components, hooks, services, and types. Features expose a public API via an index.ts barrel file; other features import from that barrel, not internal paths. Truly shared code lives in shared/components and shared/hooks. Third-party configuration lives in lib/. App-wide TypeScript types live in types/. This structure scales because changing a feature touches one folder instead of five, code ownership is clear, and features can be deleted without orphaned files scattered across the project.

When would you use Redux vs Zustand vs Context?

Context — for low-frequency global data: theme, locale, current user, feature flags. Not suitable for high-frequency updates (re-renders on every consumer). Zustand — for client UI state that is shared across many components and updated frequently. Minimal API, no boilerplate, excellent performance with its subscription model. Redux Toolkit — for large teams that need strict conventions, time-travel debugging, powerful dev tools, and explicit action history. The overhead is justified in complex applications (e-commerce carts, real-time dashboards). TanStack Query / SWR — for server state specifically (cache, background refetch, optimistic updates). These should be your first choice for async data; they eliminate most reasons to put server data in Redux.

What is the difference between server state and client state?

Server state is data that lives on the server and is fetched asynchronously: user profiles, product lists, orders. It has loading, error, and stale states. It can change on the server without your client knowing. Manage server state with TanStack Query or SWR — they handle caching, background refetching, and synchronization. Client state is local UI state that does not need to be persisted to a server: modal open/closed, selected tab, form draft. Manage it with useState, useReducer, or Zustand. Many bugs in React apps come from putting server state in Redux and manually managing loading/error flags that a library would handle automatically.

How do you handle authentication in React?

Store the authentication token in an httpOnly cookie (set by the server), not in localStorage or React state — httpOnly cookies are inaccessible to JavaScript and immune to XSS theft. Maintain the current user object in React Context or a state manager, fetched from a /me endpoint on app load. Protect routes by checking the auth state in a layout component or a route guard. On logout, call a server endpoint to invalidate the session and clear the cookie. For Next.js, consider libraries like NextAuth.js or Clerk that handle the entire session lifecycle.

What is code splitting and when should you use it?

Code splitting divides your JavaScript bundle into multiple smaller chunks that are loaded on demand. Without it, the browser downloads all of your application's code before anything renders — a poor experience for users on slow connections. When to split: always split at the route level (each page is a separate chunk). Also split large components that are only shown conditionally (rich text editors, PDF viewers, data visualization libraries, admin dashboards). How: use React.lazy(() => import('./HeavyComponent')) with Suspense for component-level splitting. Next.js and React Router handle route-level splitting automatically.

Note
Interview answers are most compelling when you support them with concrete examples from your own experience. For each answer above, think of a real scenario where you encountered this concept — that makes the difference between reciting and demonstrating expertise.
Tip
Practice explaining these concepts without code first. If you can explain reconciliation, the virtual DOM, and hooks in plain English, the code examples become easy to provide on demand.