Back to blog
Frontend Engineeringadvanced

React Interview Questions: Senior Level (4+ Years)

30 advanced React interview questions with in-depth answers for senior developers — Fiber architecture, concurrent rendering, RSC, design systems, performance profiling, and system design.

LearnixoApril 13, 202612 min read
ReactInterviewSeniorFiberConcurrentArchitectureRSCSystem Design
Share:š•

Senior-Level Signals

Senior interviewers aren't checking if you know the API. They're evaluating:

  1. System thinking — Can you design a scalable component architecture?
  2. Tradeoff reasoning — "What would you pick and why, given these constraints?"
  3. Depth under pressure — Can you explain how React actually works, not just what the docs say?
  4. Production experience — Have you debugged real performance problems, not just toy examples?

Every answer should demonstrate these signals.


React Internals & Architecture

Q1: Explain React Fiber — what problem it solved and how it works.

The problem: The old stack reconciler was synchronous and recursive. Once reconciliation started, it ran to completion — blocking the main thread for large trees and causing jank (dropped frames, unresponsive inputs).

The solution: Fiber breaks reconciliation into a linked list of "units of work" (one fiber per component). React can process a unit of work, check if there's higher-priority work (like a user input), pause, and resume later. This is called time-slicing.

How it works:

Fiber tree structure (each fiber is a JS object):
{
  type: MyComponent,
  key: null,
  stateNode: DOM node or class instance,
  return: parentFiber,    ← parent
  child: childFiber,      ← first child
  sibling: nextFiber,     ← next sibling
  effectTag: UPDATE | PLACEMENT | DELETION,
  memoizedState: hook linked list,
  pendingProps: new props,
  memoizedProps: old props,
}

Two phases:

  1. Render phase (interruptible): React traverses the fiber tree, calls component functions, builds the work-in-progress tree. Can be paused and resumed.
  2. Commit phase (synchronous): React applies all DOM mutations at once. Cannot be interrupted — partial DOM updates would leave the UI in an inconsistent state.

Practical implications: Concurrent rendering, useTransition, useDeferredValue, Suspense for data fetching, and React Server Components are all built on Fiber's interruptibility.


Q2: What is the React reconciliation algorithm and its time complexity?

React's diffing algorithm is O(n) — linear in the number of elements. It achieves this through two heuristics:

  1. Different types → full replace: If <div> becomes <span>, React unmounts the entire subtree and mounts a new one. It doesn't try to diff them.

  2. Keys for list optimization: Without keys, React matches by position — O(n) comparisons but requires re-rendering all items on insertion. With stable keys, React matches items correctly and only re-renders what changed.

Tradeoff: Generic tree diffing is O(n³). React's heuristics reduce this to O(n) at the cost of some accuracy — hence the requirement to use stable keys and avoid changing element types in the same position.


Q3: Explain the difference between useEffect and useLayoutEffect and when to use each.

Both accept the same signature. The difference is when they fire relative to the browser paint:

Render → Commit DOM mutations → useLayoutEffect → Browser Paint → useEffect

useLayoutEffect: Fires synchronously after DOM mutations, before the browser paints. The user never sees the intermediate state. Use it when:

  • Measuring DOM (offsetWidth, getBoundingClientRect) to position something
  • Preventing a visual flash (tooltip positioning, scroll restoration)

useEffect: Fires asynchronously after the browser paints. Doesn't block painting. Use it for everything else — data fetching, subscriptions, logging.

Warning: useLayoutEffect in Server Components throws an error (no DOM during SSR). Use useEffect for SSR-safe code, or conditionally run useLayoutEffect on the client.


Q4: How does React's batching work in React 18 vs React 17?

React 17: Only batched updates inside React event handlers. Updates in setTimeout, Promise.then, or native event listeners were NOT batched — each triggered a separate re-render.

React 18: Automatic batching everywhere. All state updates in a single event loop tick are batched into one re-render, regardless of origin (setTimeout, Promise, native events).

JavaScript
// React 17: THREE renders
setTimeout(() => {
  setA(1);    // render 1
  setB(2);    // render 2
  setC(3);    // render 3
}, 100);

// React 18: ONE render (automatic batching)
setTimeout(() => {
  setA(1);
  setB(2);
  setC(3);   // single render
}, 100);

// Opt out when needed
import { flushSync } from "react-dom";
flushSync(() => setA(1));  // Render immediately
setB(2);                   // Second render

Q5: What are React Server Components (RSC) and how do they differ from SSR?

Traditional SSR:

  1. Full component tree renders on server → HTML
  2. Sends HTML to browser (fast initial paint)
  3. Browser downloads JS bundle
  4. Full component tree re-renders on client (hydration)
  5. App is interactive

Problem: you ship JavaScript for every component, including ones that only render static content.

React Server Components (RSC):

  • Server components run only on the server, never on the client
  • Their JavaScript never ships to the browser
  • They can directly access databases, file systems, secrets
  • Client components handle interactivity
  • React serializes the server-rendered tree and streams it to the client
TSX
// page.tsx — Server Component (no "use client")
// Can: async/await, access DB, use secrets, import server-only packages
// Cannot: useState, useEffect, event handlers, browser APIs
export default async function BlogPage() {
  const posts = await db.posts.findMany();  // Direct DB access — no API layer!
  return <PostList posts={posts} />;
}

// PostList.tsx — can be Server or Client
// If it needs state/effects → "use client"
// If it's just rendering → Server Component (smaller bundle)

Key benefit: Zero-bundle components. A markdown renderer that imports a 100KB library: with RSC, that 100KB stays on the server.


Concurrent React

Q6: What is the difference between useTransition and useDeferredValue?

Both mark work as non-urgent, allowing React to prioritize urgent updates (like typing in an input). The difference is ownership:

useTransition: You own the state update. Wrap it in startTransition.

JSX
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
  setInputValue(value);                     // Urgent — updates input immediately
  startTransition(() => setResults(heavy(value)));  // Deferred — can be interrupted
};

useDeferredValue: You don't own the state update — it comes from a prop or parent.

JSX
function SearchResults({ query }) {  // query comes from parent
  const deferredQuery = useDeferredValue(query);
  const results = useMemo(() => filter(items, deferredQuery), [deferredQuery]);
  const isStale = query !== deferredQuery;
  return <div style={{ opacity: isStale ? 0.6 : 1 }}>{results}</div>;
}

Q7: How does Suspense for data fetching work in React 18?

Suspense works by "suspending" — when a component throws a Promise (or a library like React Query detects the query isn't cached), React catches it, shows the nearest <Suspense fallback={...}>, and retries the component when the Promise resolves.

JSX
function Profile({ userId }) {
  // useQuery with suspense: true — throws a promise if data isn't cached
  const { data: user } = useQuery({ queryKey: ['user', userId], suspense: true });
  return <div>{user.name}</div>;  // Always has data here
}

function ProfilePage() {
  return (
    <ErrorBoundary fallback={<ProfileError />}>
      <Suspense fallback={<ProfileSkeleton />}>
        <Profile userId="123" />
      </Suspense>
    </ErrorBoundary>
  );
}

Advantage over traditional loading states: You don't need if (loading) return <Skeleton> in every component. Suspense boundaries handle it declaratively. Compose multiple Suspense boundaries for granular loading states.


Design Patterns & Architecture

Q8: Design a scalable component architecture for a large React application.

Layer the architecture:

Pages (route-level)
  └── Features (domain: auth, products, checkout)
        ā”œā”€ā”€ Feature components
        ā”œā”€ā”€ Feature hooks
        ā”œā”€ā”€ Feature API layer
        └── Feature store slice
  └── Shared components (design system)
        ā”œā”€ā”€ Primitives (Button, Input, Badge)
        └── Composites (DataTable, Modal, Dropdown)
  └── Utilities (formatters, validators, cn())

Key principles:

  1. Feature colocation: Everything a feature needs lives with the feature. No shared-util sprawl.
  2. API layer abstraction: Components never call fetch directly. They use hooks that call typed API functions.
  3. Single import rule: External code imports from a feature's index.ts barrel, not from internal files.
  4. Context boundaries: Each domain has its own Context. No global mega-context.
  5. Design system boundary: UI primitives have no business logic. Features own business logic.

Q9: What is the "children as composition" pattern and how does it solve performance problems?

JSX
// Problem: <Wrapper> has fast-changing state but ExpensiveChild is in its subtree
function Wrapper() {
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  // ExpensiveChild re-renders on every mouse move!
  return (
    <div onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}>
      <ExpensiveChild />
    </div>
  );
}

// Solution: lift ExpensiveChild OUT of the state owner
function App() {
  return (
    <Wrapper>
      <ExpensiveChild />  {/* children reference is stable — doesn't re-render */}
    </Wrapper>
  );
}

function Wrapper({ children }) {
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  // children is a prop — its reference doesn't change when mousePos changes
  return (
    <div onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}>
      {children}
    </div>
  );
}

This is often better than React.memo — it doesn't require the child to know about optimization, and it works naturally without custom comparators.


Q10: When is a discriminated union better than boolean flags for component state?

Boolean flags allow impossible combinations:

TSX
// āŒ Can have loading=true AND error simultaneously
{ loading: boolean; data: User | null; error: string | null }

A discriminated union makes impossible states unrepresentable:

TSX
// āœ… Only valid combinations exist
type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };

// TypeScript narrows in switch — no defensive checks needed
switch (state.status) {
  case "success": return <Profile user={state.data} />;  // data is guaranteed here
  case "error": return <Alert message={state.error} />;  // error is guaranteed here
}

In interviews: always mention that this pattern eliminates a class of bugs rather than just being style preference.


Performance Profiling

Q11: Walk me through how you would diagnose and fix a React performance problem in production.

Step 1: Measure, don't guess.

JSX
// Add React Profiler around the suspect area
<Profiler id="DataTable" onRender={(id, phase, actualDuration) => {
  if (actualDuration > 16) analytics.track("slow_render", { id, duration: actualDuration });
}}>
  <DataTable />
</Profiler>

Step 2: Open React DevTools Profiler. Record interaction. Look for:

  • Wide bars = slow renders
  • Yellow bars = components that rendered but didn't need to
  • Check "why did this render?" panel

Step 3: Common causes and fixes: | Symptom | Cause | Fix | |---|---|---| | All children re-render on parent state change | No memoization | React.memo + useCallback | | Memoization not working | Unstable object/function props | useMemo for objects, useCallback for functions | | Long single render | Expensive computation | useMemo | | Scroll jank with long lists | Too many DOM nodes | Virtualization | | Context causing global re-renders | One large context | Split contexts by domain |

Step 4: Measure Web Vitals (real user impact):

JavaScript
import { onLCP, onINP } from "web-vitals";
onLCP(({ value, rating }) => analytics.track("LCP", { value, rating }));
onINP(({ value, rating }) => analytics.track("INP", { value, rating }));

Q12: What is the "prop stability" problem and how does it manifest?

Prop stability: when a prop's value is the same semantically but different by reference, breaking React.memo.

JSX
// Every render creates a new object and function — memo is useless
function Parent({ id }) {
  return (
    <MemoChild
      config={{ color: "blue" }}        // New object every render
      onClick={() => handleClick(id)}   // New function every render
    />
  );
}

// Solutions:
const CONFIG = { color: "blue" };  // Stable: module-level constant

function Parent({ id }) {
  const config = useMemo(() => ({ color: "blue" }), []);  // Or memoized
  const handleClick = useCallback(() => onDelete(id), [id, onDelete]);
  return <MemoChild config={config} onClick={handleClick} />;
}

In performance interviews, proactively mention prop stability as a prerequisite for React.memo working correctly.


System Design Questions

Q13: Design the state management architecture for a complex SaaS dashboard.

Separate state by type:

Server state (API data)     → React Query / RTK Query
  - Users, projects, tasks
  - Auto-cached, auto-refetched
  - Optimistic updates on mutations

UI state (no server sync)   → Zustand / Context
  - Modal open/closed
  - Active tab
  - Sidebar collapsed

Auth state                  → Context + localStorage persistence
  - Current user
  - Token
  - Role/permissions

Form state                  → React Hook Form (local, per-form)
  - Field values
  - Validation errors
  - Dirty/submitted state

URL state                   → React Router search params
  - Active filters
  - Current page
  - Sort column/direction (shareable via URL)

Why this split matters: Each type of state has different update frequency, persistence requirements, and sharing patterns. One global store tries to be everything and ends up being nothing well.


Q14: How would you implement a design system that scales across 10 teams?

Layer 1: Primitives — no business logic, full style control

TSX
// Button, Input, Badge, Modal, Tooltip
// Extend HTML props, forward refs, Tailwind-based
// Published as an internal npm package

Layer 2: Composites — combinations of primitives

TSX
// DataTable, FormField, SearchableSelect, DateRangePicker
// Include accessibility (ARIA, keyboard nav) by default

Layer 3: Domain components — business context

TSX
// UserAvatar, StatusBadge, PrioritySelector
// Import from layer 1/2, add domain semantics

Governance:

  • Changelog + semantic versioning
  • Visual regression tests (Chromatic/Storybook)
  • Accessibility CI gate (axe-core)
  • Usage analytics per component
  • Monthly design system office hours

Common failure mode: Trying to make every component fully customizable via props. Instead: allow 80% of uses out of the box, provide escape hatches (className, as-prop, compound patterns) for the other 20%.


What Separates Senior from Mid-Level

In 5+ years of React interviews, these are the separating questions:

  1. "How does React actually know when to re-render?" — Senior answer covers Fiber, referential equality, bailout optimization, not just "state changes."

  2. "Design the component architecture for..." — Seniors think in layers: primitives, composites, domain components, features. They talk about constraints before solutions.

  3. "Your useEffect is causing an infinite loop. Debug it." — Seniors immediately ask about the dependency array and whether state is being set inside the effect.

  4. "When wouldn't you use React for this?" — Seniors know React's limits. Heavy computation → Web Worker. Simple static site → Next.js static or Astro. Real-time heavy UI → consider SolidJS.

  5. "How do you know this optimization is necessary?" — Seniors profile first. They never add useMemo speculatively.

Enjoyed this article?

Explore the Frontend Engineering learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.