React Interview Questions and Answers — STAR Format Guide for 4+ Years Experience (2026)
February 26, 2026 Updated • By Surya Singh • React • JavaScript • TypeScript • Interview • Frontend
Key Takeaways
- 110 in-depth React STAR answers with real production scenarios, metrics, and what separates good from great
- 2Covers hooks architecture, performance, Server Components, state management, forms, testing, routing, accessibility, design systems
- 33 rapid-fire rounds (Hooks, Rendering & Reconciliation, Architecture) with 10 practice STAR answers
- 4E-E-A-T experience block, 8 common mistakes, 8 FAQs, and links to MERN, MEAN, SQL, AI/ML interview guides
This guide targets React specialists and full-stack developers preparing for senior roles. Every answer uses the STAR method with production scenarios—real problems, concrete actions, and measurable outcomes. Topics span hooks architecture (custom hooks, useCallback / useMemo / useRef trade-offs), performance (profiling, virtualization, useDeferredValue), React Server Components, state management (Context vs Redux vs Zustand vs React Query), and more.
Answers reference the React documentation, React APIs reference, and the Next.js documentation.
Table of Contents
- 1. Hooks Architecture — Custom Hooks, Composition, useCallback/useMemo/useRef
- 2. Performance Optimization — React.memo, useDeferredValue, react-window, Profiling
- 3. State Management — Context vs Redux vs Zustand vs React Query
- 4. React Server Components vs Client Components
- 5. Error Boundaries and Resilient UI Patterns
- 6. Forms — react-hook-form, Zod, Multi-step, Controlled vs Uncontrolled
- 7. Testing — RTL, MSW, Playwright, Snapshot Trade-offs
- 8. Routing, Code Splitting, Lazy Loading
- 9. Accessibility — ARIA, Keyboard Nav, Screen Readers, Focus Management
- 10. Design System Architecture — Component Library, Theming, Compound Components
- Rapid-Fire Practice (3 Rounds)
- From Real Experience
- Common Mistakes to Avoid
- FAQ (8 Questions)
- Related Interview Guides
1) How do you architect custom hooks and manage useCallback/useMemo/useRef trade-offs?
What interviewer evaluates: Hooks mastery beyond useState/useEffect basics—composition, cleanup, and when to use each primitive.
Situation: A B2B SaaS dashboard had 18 components duplicating the same data-fetching pattern: loading state, error state, retry logic, and stale handling. Each used different conventions—some wrapped fetchers in useCallback, others did not; three components had memory leaks from missing useEffect cleanup. Bug fixes were applied 18 times.
Task: Consolidate into reusable custom hooks with correct cleanup and stable references, while keeping each component's behaviour configurable.
Action:
- Generic
useFetchhook: Built a hook accepting a fetcher function, options (staleTime,retryCount,enabled), and returning{data, error, isLoading, refetch}. UsedAbortControllerinuseReffor request cancellation; cleanup inuseEffectreturn calledabort(). WrappedrefetchinuseCallbackwith stable deps so parent re-renders did not cause child re-fetches. - Composition over configuration: Instead of one 20-option hook, composed smaller hooks:
useDebounce(value, delay),usePagination(page, pageSize, total),useOptimisticUpdate(optimistic state + rollback). Example:const search = useDebounce(query, 300); const {data} = useFetch(() => api.search(search), {enabled: search.length > 2}); - useRef for values that don't affect UI: For a video player analytics component, used
useRefforcurrentPosition(updated 60fps)—no re-renders. UseduseStateonly forisPlaying(affects play/pause button). Rule:useStatewhen UI must reflect the value;useRefwhen logic needs it but not rendering. - useMemo only when profiling showed cost: Applied
useMemoto expensive derived data (e.g. sorted/filtered list of 2,000 items) only after React DevTools Profiler showed it as a hotspot. Avoided blanketuseMemoon simple computations—the comparison cost can outweigh the benefit. - TypeScript generics:
useFetch<T>(fetcher: () => Promise<T>)returneddata: T | null; callers got full type inference without assertions.
Result: 18 components lost an average of 42 lines each (756 lines removed). Three memory-leak bugs fixed in one place. New data-fetching components scaffolded in under 5 minutes. ESLint react-hooks/exhaustive-deps enforced correct dependency arrays.
What separates good from great: Show hook composition (small hooks combined), cleanup in useEffect, and when not to use useCallback/useMemo. Great candidates explain the cost of memoization.
2) How do you diagnose and fix React performance issues?
What interviewer evaluates: Profiling-driven optimisation, not guessing. Knowledge of React.memo, useDeferredValue, virtualization.
Situation: An e-commerce admin panel rendered a table with 3,500 SKU rows. Typing in the global search bar caused 800ms input lag. Users reported the UI felt frozen. Lighthouse performance score was 58.
Task: Bring search input latency below 100ms without reducing the dataset or removing features.
Action:
- Profiling first: Opened React DevTools Profiler, recorded a keystroke. Found that every keystroke re-rendered all 3,500
ProductRowcomponents—total render time 760ms. The search state lived in the parent; updating it re-rendered the whole tree. - Stable callbacks +
React.memo: WrappedProductRowinReact.memo. MovedonEdittouseCallbackwith stable deps. Previously,onEditwas an arrow function in JSX—new reference every render, soReact.memohad no effect. - State colocation: Moved search input into a
SearchBarcomponent with local state. UseduseDeferredValuefor the filtered list so the input stayed responsive while the table updated asynchronously. - Virtualization with react-window: Replaced the full list with
FixedSizeListfrom react-window. Only ~25 visible rows rendered. Combined with memo, each keystroke rendered 25 rows instead of 3,500. - Expensive computation: The profit-margin calculation ran on every row render. Wrapped it in
useMemowith[product.cost, product.price]as deps.
Result: Search input latency dropped from 760ms to 22ms. DOM nodes reduced from 18,000 to ~600. Lighthouse performance score improved to 92. The profiler showed zero wasted renders on keystroke.
What separates good from great: Start with profiling data, not guesses. Show the layered approach: memo + stable callbacks → state colocation → virtualization → useMemo. Mention useDeferredValue for concurrent features.
Loading...
3) How do you decide between Context, Redux, Zustand, and React Query?
What interviewer evaluates: Architectural judgment and a decision framework, not "I always use X."
Situation: A team used Redux for everything—form inputs, dropdown states, server-cached API data, and auth. The store had 95 actions and 32 reducers. Every keystroke dispatched an action; the entire store re-serialized. New developers needed 3 weeks to understand the state architecture.
Task: Simplify state architecture without a full rewrite.
Action:
- Four-tier decision framework:
- UI state (modal open, form inputs, dropdowns) →
useState/useReducerlocally. - Server state (API data, caching, sync) → React Query (
useQuery,useMutation). Caching, background refetch, optimistic updates built-in. - Shared client state (theme, sidebar collapsed, preferences) → Zustand (minimal boilerplate, no providers).
- Auth (user, permissions) → React Context with
useAuthhook (changes rarely, no perf concern).
- UI state (modal open, form inputs, dropdowns) →
- Incremental migration: Started with product search—replaced Redux with a single
useQuery. Removed 180 lines. Gained loading/error states and stale-while-revalidate for free. - Context performance guard: For auth context, wrapped the value in
useMemoto avoid unnecessary consumer re-renders when parent state changed.
Result: Redux store reduced from 95 actions to 8 (only truly global state). Bundle size dropped 22 KB. New developers understood state architecture in 3 days. React Query eliminated 100% of manual loading-state boilerplate.
What separates good from great: Present the decision framework for each tier. Explain why Redux is overkill for server cache state and why React Query exists.
4) When and why would you use React Server Components vs Client Components?
What interviewer evaluates: Understanding of the RSC model, trade-offs, and when to add "use client".
Situation: A documentation site shipped 310 KB of JavaScript for the article page. Markdown rendering (remark, rehype) ran entirely on the client during first paint and was never used again. Time to Interactive on 3G was 4.2 seconds.
Task: Reduce client-side JavaScript without losing rich markdown or data fetching.
Action:
- Default to Server Components: In Next.js App Router, components are Server Components by default. The article page became a Server Component that fetched content from the CMS and rendered markdown on the server.
- Client boundary at interactivity: Added
"use client"only to the component that needed it: the "Copy code" button, table-of-contents navigation, and comment form. The rest stayed on the server. - No props from Server to Client with serialization issues: Passed only serializable props (primitives, plain objects) to Client Components. Avoided passing functions, class instances, or DOM nodes.
- Data fetching in Server Components: Fetched directly in the Server Component—no API route or
useEffectfetch. The server rendered with data; only HTML and minimal hydration payload went to the client.
Result: Client JavaScript for the article page dropped from 310 KB to 38 KB. Time to Interactive improved by 2.1 seconds on 3G. Markdown parsing moved to the server (12ms); no client CPU cost.
What separates good from great: Explain the rule: Server Components for static/data-heavy; Client Components at the boundary where interactivity begins. Mention serialization and bundle impact.
5) How do you implement error boundaries and resilient UI patterns?
What interviewer evaluates: Understanding that error boundaries are class components, placement strategy, and recovery patterns.
Situation: A financial dashboard had 12 sections (portfolio, holdings, transactions, charts). A single component crash in the chart library (third-party) caused a full white screen. Users lost all context and had to refresh, losing unsaved filters.
Task: Contain failures so one section crashing does not bring down the entire dashboard.
Action:
- Error boundary as class component: Built a reusable
ErrorBoundaryusinggetDerivedStateFromErrorandcomponentDidCatch. Hooks cannot implement error boundaries—they must be class components. - Granular placement: Wrapped each dashboard section:
<ErrorBoundary fallback={<ChartUnavailable onRetry={...} />}><PortfolioChart /></ErrorBoundary>. Chart failure showed a fallback with "Retry" while the rest of the dashboard stayed usable. - Error reporting: In
componentDidCatch, sent the error and component stack to Sentry with user context. 100% of component errors were captured with actionable stack traces. - Recovery: Fallback UI included a "Retry" button that reset error state via
setState, triggering a re-mount of the children. Users could recover without a full page refresh.
Result: Chart crashes no longer affected other sections. Sentry captured 23 production errors in the first month; 18 were fixed. User-reported "white screen" support tickets dropped to zero.
What separates good from great: Explain that error boundaries don't catch async errors, event handlers, or SSR errors. Show placement strategy and recovery UX.
Loading...
6) How do you build forms with react-hook-form, Zod, multi-step flows, and controlled vs uncontrolled?
What interviewer evaluates: Form library choice, validation strategy, and controlled vs uncontrolled trade-offs.
Situation: A 5-step B2B onboarding form had 240 lines of manual validation (if/else). Inconsistent error messages; validation ran only on submit. Client and server used different rules—some invalid submissions reached the API.
Task: Unify validation, improve UX with instant feedback, and eliminate client/server drift.
Action:
- react-hook-form + Zod: Adopted
react-hook-formwithzodResolver. One Zod schema per step; cross-field validation via.refine(). Form validated on blur (mode: 'onBlur') for instant feedback without blocking typing. - Shared schema: The same Zod schemas validated the Express request body. One source of truth—zero drift between client and server.
- Multi-step with
useFormContext: UsedFormProvideranduseFormContextso each step component accessed form state without prop drilling. Step navigation validated current step before allowing advance. - Controlled vs uncontrolled: react-hook-form uses uncontrolled inputs by default (register)—fewer re-renders. Used controlled (
control) only for custom components (date picker, rich text) that required it.
Result: Validation code reduced from 240 lines to 58 lines of Zod schema. Form abandonment dropped 31%. Server rejected zero additional requests—all invalid data was caught on the client.
What separates good from great: Explain when controlled is necessary (custom components, dynamic fields) vs when uncontrolled is preferable (performance). Mention server-side revalidation.
7) How do you approach testing: React Testing Library, MSW, Playwright, and snapshot trade-offs?
What interviewer evaluates: Testing pyramid, user-centric tests, mocking strategy, and when to avoid snapshots.
Situation: A team had 1,200 Jest tests—mostly implementation details (state values, internal methods). Tests broke on every refactor. E2E coverage was 3 flows; regression bugs reached production weekly.
Task: Shift to user-centric testing, stable mocks, and meaningful E2E coverage.
Action:
- React Testing Library: Replaced
getByTestIdand state assertions with user-centric queries:getByRole,getByLabelText,getByPlaceholderText. Tests queried the DOM as users and assistive tech do. Avoided testing implementation (useState values, internal functions). - MSW for API mocking: Used Mock Service Worker to intercept fetch at the network level. Same handlers for unit tests and local dev. No
jest.mockof fetch—tests reflected real request/response behaviour. - Playwright for E2E: Added 12 critical flows: login, checkout, search, profile edit. Ran in CI against a staging environment. Parallel execution; full run in 8 minutes.
- Snapshot discipline: Limited snapshots to design-system components (Button, Input) that rarely change. Removed snapshots from feature components—they caused noise and discouraged refactoring.
Result: Refactors broke 80% fewer tests. Regression bugs in production dropped 70%. E2E caught 4 critical bugs before release in the first month.
What separates good from great: Explain that RTL encourages accessible markup (role-based queries). Distinguish MSW (network mock) from jest.mock (module mock).
8) How do you implement routing, code splitting, and lazy loading (React Router / Next.js App Router)?
What interviewer evaluates: Code-splitting strategy, lazy loading, route-based chunks, and Next.js App Router conventions.
Situation: A SaaS app had a single 2.1 MB initial bundle. The admin panel (15% of users) loaded the same bundle as the customer-facing app. First Contentful Paint was 3.8 seconds on 3G.
Task: Reduce initial load and split by route so admin code loads only when needed.
Action:
- Route-based code splitting: With React Router, used
React.lazyandSuspense:const AdminDashboard = React.lazy(() => import('./AdminDashboard'));Wrapped each lazy route in<Suspense fallback={<PageSkeleton />}>. - Next.js App Router: In Next.js, each route segment is automatically code-split. The
app/adminfolder only loads when the user navigates to/admin. No manualReact.lazyneeded. - Prefetching: Next.js prefetches visible link hrefs by default. For React Router, added prefetch on
Linkhover for critical paths (e.g. checkout). - Loading states: Designed route-level skeletons that matched the final layout. Avoided layout shift when lazy chunks loaded.
Result: Initial bundle dropped from 2.1 MB to 420 KB. Admin panel loaded on-demand (380 KB chunk). FCP improved to 1.2 seconds. 85% of users never downloaded admin code.
What separates good from great: Explain the relationship between React.lazy, Suspense, and dynamic import. Mention Next.js automatic splitting and prefetch.
Loading...
9) How do you implement accessibility: ARIA, keyboard navigation, screen readers, focus management?
What interviewer evaluates: ARIA usage, keyboard support, focus trap/restore, and testing with assistive tech.
Situation: A modal dialog was not keyboard-accessible; focus escaped to content behind it. Screen readers announced the modal only after navigating through the entire page. The product had to meet WCAG 2.1 AA for a enterprise customer.
Task: Make modals, dropdowns, and navigation fully accessible.
Action:
- Focus trap in modals: On modal open, stored the previously focused element. Used
useEffectto move focus to the first focusable element. On Tab, cycled focus within the modal (last element Tab → first element). On Escape, closed the modal and restored focus to the stored element. - ARIA attributes: Added
role="dialog",aria-modal="true",aria-labelledby(title),aria-describedby(description). For dropdowns:aria-expanded,aria-haspopup,aria-controls. - Live regions: For dynamic success/error messages, used
aria-live="polite"androle="status"so screen readers announced updates without navigating. - Testing: Ran axe-core in CI. Manual testing with NVDA and VoiceOver. Verified keyboard-only navigation (no mouse) for all flows.
Result: Modal passed WCAG 2.1 AA. Focus restoration worked for keyboard and screen reader users. axe-core caught 12 issues in CI; all resolved before merge. Enterprise customer signed after accessibility audit.
What separates good from great: Mention focus trap libraries (e.g. focus-trap-react) vs manual implementation. Explain when to use aria-live vs aria-atomic.
10) How do you architect a design system: component library, theming, compound components?
What interviewer evaluates: Component API design, compound component pattern, theming approach.
Situation: A company had 4 products with duplicated Button, Input, Modal components—each with slightly different styling and behaviour. Changing a design decision required updates in 12 places. No single source of truth.
Task: Build a shared component library with theming and flexible composition.
Action:
- Compound components: For complex components like
Select, used the compound pattern:<Select><Select.Trigger>...</Select.Trigger><Select.Content><Select.Item>...</Select.Item></Select.Content></Select>. Context shared state; each sub-component rendered its part. Consumers got full control over structure and styling. - CSS variables for theming: Defined design tokens as CSS variables (
--color-primary,--spacing-md). Components consumed variables; switching theme changed the root. No prop drilling of theme objects. - Variant API:
Buttonacceptedvariant(primary, secondary, ghost),size(sm, md, lg), andasChildfor polymorphic rendering (render as Link when needed). - Documentation and usage: Used Storybook for documentation. Each component had usage guidelines and accessibility notes. ESLint plugin warned when design-system imports were used incorrectly.
Result: 4 products migrated to the library in 6 weeks. Design changes applied in one place. Bundle size per product decreased 15% (shared chunks). New components scaffolded from Storybook templates.
What separates good from great: Explain the compound component pattern and when it beats a single large component with many props. Mention polymorphic asChild for flexibility.
Rapid-fire interview practice — STAR answers
60-second verbal answers. Practice out loud.
Round 1: Hooks Deep Dive (4 questions)
Q: When do you need useEffect cleanup and how do you implement it?
Situation: A real-time dashboard subscribed to WebSocket events. When users navigated away, the subscription was not cleaned up—memory leaks and duplicate handlers after returning.
Task: Ensure subscriptions are cleaned up when the component unmounts.
Action: Return a cleanup function from useEffect: return () => socket.off('update', handler); For fetch, used AbortController: const ac = new AbortController(); fetch(url, {signal: ac.signal}); return () => ac.abort(); For intervals/timeouts, returned clearInterval or clearTimeout. Added all subscriptions to the dependency array so cleanup ran when deps changed (e.g. roomId).
Result: Zero memory leaks. Navigating away and back no longer duplicated handlers. React Strict Mode double-invoked effects, exposing 2 missed cleanups during development.
Q: When would you use useLayoutEffect instead of useEffect?
Situation: A tooltip positioned itself based on the target element's getBoundingClientRect. With useEffect, the tooltip briefly flashed in the wrong position (0,0) before jumping—visible flicker.
Task: Position the tooltip before the browser paints.
Action: Switched to useLayoutEffect. It runs synchronously after DOM mutations but before paint. The tooltip read the target rect and set its position in the same frame. No visible jump. Used only for DOM measurements and synchronous DOM updates that affect layout. Avoided heavy work in useLayoutEffect—it blocks painting.
Result: Tooltip appeared in the correct position with zero flicker. Used useLayoutEffect only for this specific case; everything else stayed in useEffect.
Q: How do you use useImperativeHandle?
Situation: A parent needed to call focus() and scrollIntoView() on a custom SearchInput component. Without a ref, the parent had no way to imperatively control the child.
Task: Expose a minimal imperative API to the parent via ref.
Action: Wrapped the component in forwardRef. Used useImperativeHandle to expose only { focus, scrollIntoView }—not the full DOM node. The parent could do searchRef.current.focus() without accessing internal implementation. Exposed the smallest API necessary; avoided exposing the entire DOM element.
Result: Parent could trigger focus and scroll when opening a search overlay. The encapsulation boundary stayed clean—parent did not depend on internal DOM structure.
Q: How do you design a custom hook that composes with others?
Situation: Multiple components needed debounced search that triggered a fetch, with loading and error states.
Task: Build a reusable hook that composes smaller hooks.
Action: Created useDebouncedSearch that internally used useDebounce (value, delay) and useFetch (fetcher, enabled). Returned { query, results, isLoading, error, setQuery }. Each primitive hook had a single responsibility; the composed hook combined them. The component only called useDebouncedSearch—one line instead of 15. Kept the composed hook thin; complex logic stayed in primitive hooks.
Result: 6 components adopted the hook. Changes to debounce delay or fetch logic happened in one place. New search UIs were built in minutes.
Round 2: Rendering & Reconciliation (4 questions)
Q: How does the virtual DOM and reconciliation work?
Situation: Interview question on React internals.
Task: Explain virtual DOM and reconciliation.
Action: The virtual DOM is a lightweight JS representation of the UI. On state change, React builds a new tree, diffs it with the previous one (reconciliation), and computes the minimal DOM updates. The Fiber architecture allows work to be split into chunks and prioritized. Reconciliation uses heuristics: same element type and key mean reuse; different key means unmount/remount. Keys should be stable and unique—never array index for reorderable lists.
Result: Demonstrated understanding of why React uses this model: declarative API, batching, and predictable updates. Avoided the myth that "virtual DOM is always faster than direct DOM."
Q: What is the Fiber architecture and why does it matter?
Situation: A product manager asked why React 18 could pause and resume rendering.
Task: Explain Fiber and concurrent rendering.
Action: Fiber is the unit of work in React—each component instance has a Fiber node. The Fiber tree is a linked list (with child, sibling, return pointers) that allows work to be paused, resumed, and prioritized. React can interrupt low-priority updates (e.g. rendering search results) for high-priority ones (e.g. typing in the input). This enables features like useTransition and useDeferredValue. The previous stack reconciler could not pause.
Result: Explained the shift from synchronous recursive rendering to incremental work. Mentioned that Concurrent Mode is opt-in via these APIs.
Q: How does Suspense work and when do you use it?
Situation: A page had multiple async data sources; we wanted to show a loading state for each independently without waterfall loading.
Task: Use Suspense for declarative loading states.
Action: Wrapped lazy-loaded components and data-fetching boundaries in Suspense. When a child suspended (e.g. React Query with suspense mode, or React.lazy), React showed the nearest fallback. Multiple Suspense boundaries allowed progressive loading—header first, sidebar second, main content last. Combined with useTransition for non-blocking transitions. Suspense only works with libraries that support it (React Query, Relay) or React.lazy.
Result: Page shell appeared in 200ms; content streamed in as data resolved. No spinners for already-cached data—React Query integrated with Suspense to avoid unnecessary fallbacks.
Q: What is concurrent rendering and how do useTransition/useDeferredValue fit in?
Situation: A search input re-rendered a large filtered list on every keystroke. The input felt laggy because React had to finish the list render before responding to the next keystroke.
Task: Keep the input responsive while deferring the expensive list update.
Action: Used useDeferredValue(filter) to derive a deferred version of the filter. The list read the deferred value; the input read the real value. React could interrupt the list render to process the next input, then resume. Alternatively, useTransition for explicit transitions: startTransition(() => setFilter(value))—updates inside were deprioritized. Both rely on concurrent rendering to interleave work.
Result: Input latency dropped from 300ms to under 50ms. The list updated slightly later but the UX felt responsive. Used for non-urgent UI updates.
Round 3: Architecture & Patterns (4 questions)
Q: HOC vs render props vs hooks—when do you use each?
Situation: Need to share logic (e.g. auth check, data fetching) across components.
Task: Choose the right pattern.
Action: Hooks first—they compose naturally, avoid wrapper hell, and keep logic co-located. Used useAuth, useFetch for most cases. Render props when the consumer needs full control over rendering and the pattern is one-off (e.g. a resize observer). HOC only for cross-cutting concerns that must wrap the entire component tree (e.g. theme provider, error boundary wrapper). In modern codebases, hooks replace 90% of HOC and render-prop use cases.
Result: New shared logic implemented as hooks. One legacy HOC remained for analytics wrapping. Render props used for a single ResizeObserver component.
Q: How do you implement feature flags in a React app?
Situation: A/B tests and gradual rollouts required feature flags. The previous approach injected flags via window and caused layout shift when flags loaded.
Task: Integrate feature flags without flash of wrong content.
Action: Loaded flags in the root layout (e.g. Next.js) or a provider. Used a context or React Query to cache flags. Components read via useFeatureFlag('newCheckout'). For SSR, fetched flags on the server and passed as initial data—no client fetch for first paint. For flags that affect layout, used CSS or conditional render with a loading state to avoid shift. Flags were evaluated once per request; no per-render network calls.
Result: Flags controlled 8 features. Rollout percentage and user targeting worked without deploy. Zero layout shift; flags available before first paint.
Q: How do you structure a React monorepo with shared packages?
Situation: 3 apps (marketing, dashboard, admin) shared components and utils. Copy-paste led to drift; no single version of a component.
Task: Set up a monorepo with shared packages.
Action: Used Turborepo (or Nx). Packages: @company/ui (design system), @company/utils, @company/config-eslint. Apps depended on packages via workspace protocol. turbo run build cached and parallelized. Changes to ui triggered rebuilds of dependent apps. Used TypeScript project references for fast incremental builds. Published internal packages to a private registry for non-monorepo consumers.
Result: Shared components had one source of truth. Build time for a single app change dropped 40% (caching). New apps could be scaffolded from templates in minutes.
Q: How do you ensure composition over prop drilling in a large React tree?
Situation: A settings panel passed 12 props through 4 levels. Adding a new setting required touching 5 files.
Task: Simplify the component API using composition.
Action: Restructured as compound components: <Settings><Settings.Section title="Profile"><Settings.Field label="Name" /></Settings.Section></Settings> Context passed the shared state (form instance, theme) to descendants. Each Settings.Field pulled what it needed from context—no props. Slots for flexibility: <Settings.Section title="..." actions={<Button>Save</Button>}> Parents composed the tree; children stayed dumb and focused.
Result: Adding a new setting required one new Settings.Field in the right section. Prop drilling eliminated. The pattern scaled to 40+ settings across 8 sections.
Loading...
From real experience
"I've conducted hundreds of React interviews over the past 6 years—from startups to FAANG. The biggest differentiator at the 4+ year level: candidates who can trace a user action through the component tree, state updates, and re-renders, and explain why a certain approach is faster or more maintainable. It's not about memorizing every hook—it's about knowing when to use which tool and being able to debug performance with the profiler."
"For hooks: stop reaching for useEffect to sync data. If you're fetching in useEffect with manual loading state, you're probably missing React Query or Server Components. And if you're using Redux for server-cache state, you're solving a solved problem. The best React codebases I've seen use React Query for server state, Zustand for tiny shared client state, and plain useState for everything else. Simplicity wins."
— Surya Singh, Senior Software Engineer & Technical Interviewer with 8+ years in React and frontend architecture
Common interview mistakes to avoid
- Claiming you know React hooks without explaining
useCallback/useMemo/useReftrade-offs—and when not to use them. - Using
useEffectfor data fetching without considering React Query or Server Components. Interviewers will ask "why not use a library built for that?" - Putting
React.memoeverywhere without profiling. "I memoized everything" is not a strategy; show you profile first and fix bottlenecks. - Recommending Redux for every project. Demonstrate the decision framework: when is Redux the right choice vs Zustand vs React Query vs Context?
- Ignoring accessibility. Senior roles expect ARIA, keyboard navigation, and focus management—especially for modals and forms.
- Not knowing the difference between Server Components and Client Components, or when to add
"use client". - Testing implementation details (state values, internal functions) instead of behaviour. Mention React Testing Library and user-centric queries.
- Skipping the "Result" with metrics in STAR answers. Interviewers want numbers: "latency dropped from X to Y," "bundle size reduced by Z%."
Frequently asked questions
What React topics are tested in senior interviews (4+ years)?
Hooks architecture with useCallback, useMemo, useRef trade-offs; custom hooks and composition; React Server Components vs Client Components; performance optimization (React.memo, useDeferredValue, virtualization); state management decision framework (Context vs Redux vs Zustand vs React Query); error boundaries; forms with react-hook-form and Zod; testing with React Testing Library and MSW; routing and code splitting; accessibility; and design system architecture.
When should I use React Server Components vs Client Components?
Use Server Components for static content, data fetching, and heavy libraries (markdown parsers, date utilities) that do not need interactivity. Use Client Components (with "use client") for anything requiring useState, useEffect, event handlers, browser APIs, or third-party client-only libraries. The rule of thumb: default to Server Components and add "use client" only at the boundary where interactivity begins. Server Components reduce client-side JavaScript and improve TTI.
How do I explain useCallback vs useMemo vs useRef in an interview?
useCallback memoizes functions to prevent child re-renders when passing callbacks to memoized components. useMemo memoizes computed values to avoid expensive recalculation on every render. useRef persists values across renders without triggering re-renders—use for DOM refs, timers, or values needed by logic but not by the UI. The key: useCallback/useMemo have a cost (comparison + storage); use them only when profiling shows they fix a measurable problem. Overuse creates maintenance burden.
What state management approach should I recommend for a new React project?
Start with React Query for server state (API data, caching, mutations) and useState/useReducer for local UI state. Add Zustand only for shared client state (theme, sidebar state, user preferences) when prop drilling or Context re-renders become a problem. Use Redux only when you need time-travel debugging, complex middleware, or a large team with established Redux patterns. React Query alone eliminates most Redux use cases for typical CRUD applications.
How important is accessibility in React interviews?
Increasingly critical at the 4+ year level. Interviewers expect knowledge of ARIA roles and attributes, keyboard navigation (focus management, trap focus in modals), screen reader considerations (live regions, semantic HTML), and focus restoration after route changes. Mention testing with axe-core, VoiceOver/NVDA, and the accessibility tree. Real projects have legal and UX requirements—demonstrating accessibility awareness separates senior candidates.
What testing strategy should a senior React developer use?
Unit tests with React Testing Library (user-centric queries, avoid implementation details). Integration tests with MSW to mock APIs. E2E tests with Playwright for critical flows. Snapshot tests sparingly—only for stable design system components, not for frequently changing UIs. Test behaviour, not implementation. Aim for high coverage on business logic; avoid testing third-party library internals.
How do I explain the virtual DOM and reconciliation in an interview?
The virtual DOM is an in-memory representation of the UI. React compares the previous and new virtual DOM trees (reconciliation), computes the minimal diff, and applies only the necessary DOM updates (commit phase). The Fiber architecture allows incremental rendering and prioritization. Mention that reconciliation is O(n) via heuristics (same type + key), and that keys should be stable and unique. Avoid saying "virtual DOM is faster than direct DOM"—it enables declarative programming and batching; the real win is React's diffing algorithm.
What are the most common React performance anti-patterns?
Creating new object/array/function references in render (causing unnecessary child re-renders), putting state too high (cascading re-renders), missing dependencies in useEffect (stale closures), overusing useEffect for data fetching (use React Query or Server Components), rendering large lists without virtualization, and premature optimization with React.memo everywhere. The correct approach: profile first with React DevTools, identify the bottleneck, then apply targeted fixes.
Loading...
Related interview guides
- MERN Stack Interview Questions — STAR Format Guide (MongoDB, Express, React, Node.js)
- MEAN Stack Interview Questions — STAR Format Guide (MongoDB, Express, Angular, Node.js)
- SQL Full Stack Developer Interview Questions (SQL Server, .NET 10, React, Azure SQL)
- AI/ML Engineer Interview Questions — STAR Format Guide
- Top 50 GenAI/LLM Interview Questions (with Practical Answers)
Surya Singh
Azure Solutions Architect & AI Engineer
Microsoft-certified Azure Solutions Architect with 8+ years in enterprise software, cloud architecture, and AI/ML deployment. I build production AI systems and write about what actually works—based on shipping code, not theory.
- Microsoft Certified: Azure Solutions Architect Expert
- Built 20+ production AI/ML pipelines on Azure
- 8+ years in .NET, C#, and cloud-native architecture