Master these 31 carefully curated interview questions to ace your next React interview.
React is a JavaScript library for building user interfaces using reusable components and a virtual DOM for efficient rendering.
Created by Facebook in 2013, React uses a component-based architecture where UI is broken into reusable pieces. It uses a Virtual DOM to minimize expensive real DOM operations. React's declarative approach means you describe what the UI should look like, and React handles the updates. It's used for SPAs, mobile apps (React Native), and even desktop apps.
JSX is a syntax extension that allows writing HTML-like code in JavaScript, which gets compiled to React.createElement() calls.
JSX stands for JavaScript XML. It's not valid JavaScript — Babel transpiles it to React.createElement(type, props, children) calls. JSX expressions must have one root element (or use fragments <>). You can embed JavaScript expressions with {}. JSX prevents XSS by escaping values by default. Class becomes className, for becomes htmlFor. JSX is optional but makes React code more readable.
Props are read-only data passed from parent to child; state is mutable data managed within a component.
Props flow top-down (parent to child) and are immutable within the receiving component. State is local to a component, managed via useState hook. Changing state triggers re-render. Props can include callbacks for child-to-parent communication. Lifting state up means moving shared state to the nearest common ancestor. Context API or state management libraries handle cross-component state.
Hooks are functions that let you use state, lifecycle, and other React features in functional components.
Introduced in React 16.8. Key hooks: useState (state management), useEffect (side effects), useContext (context consumption), useRef (mutable refs), useMemo/useCallback (memoization), useReducer (complex state). Rules: only call at top level (no conditionals/loops), only call in React functions. Custom hooks extract reusable stateful logic — must start with 'use'.
The Virtual DOM is a lightweight in-memory representation of the real DOM that React uses to calculate minimal updates.
When state changes, React creates a new Virtual DOM tree, diffs it against the previous one (reconciliation), and computes the minimum set of real DOM changes needed. This is faster than directly manipulating the DOM. React batches updates for performance. The diffing algorithm is O(n) using heuristics: different element types produce different trees, and keys identify stable elements in lists.
Controlled components have form values managed by React state; uncontrolled components use DOM refs to read values.
Controlled: input value is set by state (value={state}), onChange updates state. React is the 'single source of truth.' Uncontrolled: input maintains its own state, use useRef to read values when needed (like on submit). Controlled gives more control (validation, formatting) but more boilerplate. Uncontrolled is simpler for quick forms. React Hook Form uses uncontrolled approach for performance.
useEffect runs side effects after render. The cleanup function runs before the next effect and on unmount to prevent memory leaks.
useEffect(callback, deps) — callback runs after render. With [] deps, runs once on mount. With [dep], runs when dep changes. No deps array means every render. The return function is cleanup: useEffect(() => { const sub = subscribe(); return () => sub.unsubscribe(); }, []). Cleanup prevents memory leaks from subscriptions, timers, event listeners. React 18 strict mode runs effects twice in dev.
React.memo is a HOC that prevents re-renders when props haven't changed, using shallow comparison.
Wrap a component: export default React.memo(MyComponent). It performs shallow comparison of props. If props are the same, it skips re-rendering. Use when: component renders the same output for same props, renders often, re-rendering is expensive. Don't use for: components that always get different props, simple/cheap components. Pass a custom comparison function as second argument for deep comparison.
Context API provides a way to pass data through the component tree without prop drilling, ideal for global state like themes.
Create with createContext(), provide with <Context.Provider value={data}>, consume with useContext(Context). Best for: theme, locale, auth state, feature flags. Not ideal for: frequently changing data (causes all consumers to re-render). For complex state, combine with useReducer. React 19 introduces use() for consuming context. Consider state management libraries (Zustand, Jotai) for highly dynamic state.
React's reconciliation algorithm compares virtual DOM trees using a heuristic O(n) diff algorithm with element types and keys.
Two assumptions: (1) Different element types produce entirely different trees. (2) Keys identify stable elements across renders. When diffing: same type elements keep the DOM node and update attributes; different types unmount the old tree and mount new one. Lists use keys to match children — stable keys (like IDs) prevent unnecessary re-mounting. Never use array index as key for reorderable lists.
Code splitting breaks the bundle into smaller chunks loaded on demand using React.lazy() and dynamic import().
React.lazy(() => import('./Component')) creates a lazy-loaded component. Wrap with <Suspense fallback={<Loading/>}> to show a fallback while loading. Route-based splitting is most common: lazy-load each route. Also split large libraries, modals, and below-the-fold content. Webpack/Vite automatically creates separate chunks. This reduces initial bundle size and improves Time to Interactive.
useMemo caches computed values; useCallback caches function references to prevent unnecessary re-renders.
useMemo(() => expensiveComputation(a, b), [a, b]) — recomputes only when deps change. useCallback((args) => doSomething(a, args), [a]) — returns same function reference when deps haven't changed. useCallback is equivalent to useMemo(() => fn, deps). Use useCallback when passing callbacks to memoized children. Don't prematurely optimize — profiling should guide usage. React Compiler (React 19) may auto-memoize in the future.
Server Components render on the server, reducing client bundle size by keeping server-only code out of the browser.
RSCs run only on the server — they can directly access databases, file systems, and APIs without exposing secrets. They don't add to the client JS bundle. Can't use hooks, event handlers, or browser APIs. Client Components (marked with 'use client') handle interactivity. RSCs can import Client Components but not vice versa. Next.js App Router uses RSCs by default. Benefits: smaller bundles, faster initial load, direct backend access.
Fiber is React's reconciliation engine that enables incremental rendering by breaking work into units that can be paused and resumed.
Fiber replaces the old stack-based reconciler (React 16+). Each element becomes a 'fiber' node in a linked list tree. Work is split into units with priorities: urgent (user input) vs non-urgent (data fetching). The engine can pause low-priority work, handle urgent updates, then resume. This enables concurrent features: Suspense, transitions, selective hydration. Each fiber tracks its own state, effects, and child/sibling/return pointers.
Suspense lets components wait for async data/code by showing fallback UI until the content is ready.
Wrap async components with <Suspense fallback={<Loading/>}>. When a child 'suspends' (throws a Promise), React shows the fallback. Once resolved, React retries rendering. Works with: React.lazy() for code splitting, data fetching libraries (React Query, SWR with Suspense mode), and Server Components. Nested Suspense boundaries create loading hierarchies. startTransition can prevent existing content from being hidden during transitions.
Hydration attaches event handlers to server-rendered HTML. Selective hydration prioritizes interactive components.
SSR sends HTML to the browser for fast initial render. Hydration makes it interactive by attaching React's event system. Traditional hydration is all-or-nothing. React 18's selective hydration (with Suspense) hydrates components independently — user interactions trigger priority hydration on clicked components. Streaming SSR sends HTML progressively. This reduces Time to Interactive and improves perceived performance.
Error boundaries are class components that catch rendering errors in their subtree and display fallback UI.
Implement componentDidCatch(error, info) and static getDerivedStateFromError(error). They catch errors during rendering, lifecycle methods, and constructors of child components. They DON'T catch: event handler errors, async errors, server-side errors, or errors in the boundary itself. Place boundaries around routes, features, or individual widgets. Libraries like react-error-boundary provide hooks-based API. Log errors to monitoring services.
useReducer manages complex state logic with actions and a reducer function, similar to Redux pattern.
const [state, dispatch] = useReducer(reducer, initialState). Reducer: (state, action) => newState. Dispatch triggers state updates. Prefer useReducer when: state logic is complex, next state depends on previous, multiple sub-values, state updates are shared between handlers. useReducer + Context replaces simple Redux setups. Dispatch is stable (doesn't change between renders), so it's safe to pass to children without useCallback.
Use React DevTools Profiler to identify expensive renders, then apply React.memo, useMemo, useCallback, and state restructuring.
Steps: (1) Profile with React DevTools to find which components re-render unnecessarily. (2) Memoize expensive components with React.memo. (3) Stabilize callback props with useCallback. (4) Cache expensive computations with useMemo. (5) Move state closer to where it's used (avoid lifting too high). (6) Split context to prevent broad re-renders. (7) Use virtualization for long lists (react-window). (8) Consider state management libraries with selective subscriptions.
Use Context API for simple cases, or a state management library like Zustand/Jotai for complex, frequently updating state.
Options ranked by complexity: (1) Lift state to common ancestor (simplest). (2) Context API + useReducer for moderate global state. (3) Zustand/Jotai for lightweight but powerful state with selective re-renders. (4) Redux Toolkit for large apps with complex state logic, middleware, and devtools. (5) React Query/SWR for server state (caching, refetching). Choose based on: frequency of updates, number of consumers, need for devtools/middleware.
Use JWT tokens stored in httpOnly cookies, with a Context-based auth provider and protected route components.
Architecture: (1) AuthContext provides user state, login/logout functions. (2) Login sends credentials to API, receives JWT. (3) Store token in httpOnly cookie (not localStorage — XSS risk). (4) Axios interceptor attaches token to requests. (5) ProtectedRoute component checks auth state, redirects to login if unauthenticated. (6) Refresh token rotation for session management. (7) Server validates JWT on every request. Libraries: NextAuth.js, Auth0, Clerk simplify this.
Use Operational Transformation (OT) or CRDTs with WebSocket connections to synchronize document state across users.
Approach: (1) WebSocket connection per client for real-time sync. (2) CRDT (Conflict-free Replicated Data Type) or OT algorithm resolves concurrent edits. (3) Each keystroke generates an operation sent to server. (4) Server broadcasts to other clients and applies to canonical document. (5) Cursor positions tracked and displayed for each user. (6) Libraries: Yjs (CRDT), ShareDB (OT). (7) React renders the document with contentEditable or a rich text library (Slate, TipTap).
Keys help React identify which items changed, were added, or removed, enabling efficient list reconciliation.
Without keys, React re-renders all list items when order changes. With stable keys (like database IDs), React can match old and new items, reusing DOM nodes and preserving component state. Index keys cause bugs when items are reordered (state attaches to wrong items). Keys should be: stable, unique among siblings, and derived from data (not random). The key prop is not passed to the component as a prop.
Use atomic design principles, TypeScript, Storybook for documentation, and a monorepo with tree-shakeable exports.
Architecture: (1) Atomic design: atoms (Button, Input), molecules (SearchBar), organisms (Header). (2) TypeScript for type safety and API documentation. (3) Storybook for visual testing and documentation. (4) CSS-in-JS or CSS Modules for scoped styles. (5) Compound component pattern for flexible APIs. (6) Monorepo (Turborepo) for packages. (7) Tree-shakeable ESM exports. (8) Automated visual regression tests (Chromatic). (9) Semantic versioning with changesets.
Optimize LCP with SSR/image optimization, FID with code splitting, and CLS with proper layout reservations.
LCP (Largest Contentful Paint): SSR/SSG for fast initial render, optimize hero images (WebP, srcset, priority loading), preload fonts. FID/INP (Interaction to Next Paint): code splitting to reduce JS, defer non-critical work, use concurrent features (startTransition). CLS (Cumulative Layout Shift): set explicit dimensions on images/videos, avoid dynamically injected content above the fold, use CSS contain. Monitor with web-vitals library and Lighthouse.
RSC render on the server with zero client JS bundle, while SSR renders HTML on server but still ships component JS to client.
RSC run exclusively on the server — their code never reaches the client bundle. Benefits: direct database/filesystem access, zero bundle size for server components, streaming rendering. SSR renders HTML on server but hydrates on client (ships all JS). RSC can be mixed with client components ('use client' directive). RSC cannot use hooks, event handlers, or browser APIs. They reduce bundle size significantly. Next.js App Router uses RSC by default.
Fiber is React's incremental rendering engine that breaks rendering into units of work, enabling prioritization and interruption.
Fiber replaces the old stack-based reconciler. Each component becomes a 'fiber node' in a linked list tree. Work is split into small units that can be paused, resumed, or aborted. Priorities: Immediate (input), UserBlocking (click handlers), Normal (data fetching), Low (analytics), Idle (prefetch). Two-phase: render phase (interruptible, builds work-in-progress tree) and commit phase (synchronous, applies DOM changes). Enables concurrent features: useTransition, Suspense, startTransition.
Use React DevTools Profiler, React.memo, useMemo, useCallback, and restructure state to minimize rerenders.
Diagnosis: (1) React DevTools Profiler — identify components rerendering and why. (2) console.log in render to track frequency. (3) React.StrictMode double-renders in dev. Fixes: (1) React.memo for components receiving same props. (2) useMemo for expensive computations. (3) useCallback for function props. (4) Lift state up or colocate state closer to where it's used. (5) Split context into smaller contexts. (6) Use refs for values that don't need rerenders. (7) Virtualize long lists. (8) Avoid creating objects/arrays inline in JSX.
Use React.lazy with Suspense for route-based splitting, dynamic imports for feature modules, and bundle analysis.
Strategies: (1) Route-based: React.lazy(() => import('./Page')) with Suspense boundary per route. (2) Component-based: lazy load below-the-fold components, modals, heavy charts. (3) Library-based: dynamic import heavy libraries (moment, lodash) only when needed. (4) Prefetching: <link rel='prefetch'> for likely next routes. (5) Named exports with barrel files cause large bundles — import directly from module files. (6) Analyze with webpack-bundle-analyzer or @next/bundle-analyzer. (7) Set performance budgets in CI.
React 18 automatically batches all state updates in any context, while React 17 only batched inside event handlers.
React 17: setState calls in event handlers were batched (one rerender). But setTimeout, Promises, native events triggered separate rerenders per setState. React 18: Automatic batching everywhere — event handlers, setTimeout, Promises, native events all batch by default. Use flushSync() to opt out and force immediate rerender. Batching groups multiple setState calls into a single rerender for performance. This is why you can call setState multiple times and React renders once with the final state.
Ready to master React?
Start learning with our comprehensive course and practice these questions.