React Development · Lesson 15 of 15
Interview Prep: Senior (50 Q)
Senior-Level Signals
Senior interviewers aren't checking if you know the API. They're evaluating:
- System thinking — Can you design a scalable component architecture?
- Tradeoff reasoning — "What would you pick and why, given these constraints?"
- Depth under pressure — Can you explain how React actually works, not just what the docs say?
- 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:
- Render phase (interruptible): React traverses the fiber tree, calls component functions, builds the work-in-progress tree. Can be paused and resumed.
- 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:
-
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. -
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 → useEffectuseLayoutEffect: 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).
// 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 renderQ5: What are React Server Components (RSC) and how do they differ from SSR?
Traditional SSR:
- Full component tree renders on server → HTML
- Sends HTML to browser (fast initial paint)
- Browser downloads JS bundle
- Full component tree re-renders on client (hydration)
- 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
// 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.
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.
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.
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:
- Feature colocation: Everything a feature needs lives with the feature. No shared-util sprawl.
- API layer abstraction: Components never call
fetchdirectly. They use hooks that call typed API functions. - Single import rule: External code imports from a feature's
index.tsbarrel, not from internal files. - Context boundaries: Each domain has its own Context. No global mega-context.
- 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?
// 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:
// ❌ Can have loading=true AND error simultaneously
{ loading: boolean; data: User | null; error: string | null }A discriminated union makes impossible states unrepresentable:
// ✅ 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.
// 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):
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.
// 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
// Button, Input, Badge, Modal, Tooltip
// Extend HTML props, forward refs, Tailwind-based
// Published as an internal npm packageLayer 2: Composites — combinations of primitives
// DataTable, FormField, SearchableSelect, DateRangePicker
// Include accessibility (ARIA, keyboard nav) by defaultLayer 3: Domain components — business context
// UserAvatar, StatusBadge, PrioritySelector
// Import from layer 1/2, add domain semanticsGovernance:
- 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:
-
"How does React actually know when to re-render?" — Senior answer covers Fiber, referential equality, bailout optimization, not just "state changes."
-
"Design the component architecture for..." — Seniors think in layers: primitives, composites, domain components, features. They talk about constraints before solutions.
-
"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.
-
"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.
-
"How do you know this optimization is necessary?" — Seniors profile first. They never add
useMemospeculatively.