⚛ Expert React Interview Questions
This lesson is designed for senior and lead-level roles. Topics include React Fiber, Concurrent Mode, Server Components, the stale closure problem, micro-frontends, React Query, advanced patterns, and React 18/19 features. These questions separate engineers who use React from those who truly understand it.
Questions & Answers
01 Explain React Fiber architecture and why it was introduced. ►
Internals React Fiber (React 16) was a complete rewrite of React’s core reconciliation engine. The old “Stack Reconciler” processed updates as a single synchronous operation โ once started, it couldn’t be interrupted, which caused dropped frames and janky UIs on complex updates.
What Fiber introduces:
- Unit of work: Each component becomes a “fiber node” โ a JavaScript object representing a unit of work with properties: type, props, state, effects, and a link to its parent, child, and sibling fibers.
- Incremental rendering: Fiber can split rendering work into chunks and spread it over multiple frames. It can pause work and resume it later.
- Priority scheduling: Different updates have different priorities (user input > data fetch > background). Fiber uses a scheduler to process high-priority updates first.
- Two phases: Render phase (pure, interruptible โ builds the work-in-progress tree) and Commit phase (synchronous, applies DOM changes).
Fiber is the foundation that makes Concurrent Mode, Suspense, and streaming SSR possible. Without it, none of those features could exist.
02 What is Concurrent Mode and what problems does it solve? ►
Concurrent Concurrent Mode (now called “concurrent features” in React 18) is a set of capabilities that allows React to prepare multiple versions of the UI simultaneously and interrupt, pause, or abandon renders in favour of more urgent work.
Problems it solves:
- Input latency: In traditional React, a slow render (e.g., typing in a search that filters 10,000 items) blocks the browser from processing user input. Concurrent React can interrupt the slow render when a keystroke arrives.
- Waterfall loading: Components that need data no longer need to block everything โ Suspense lets them show placeholders while data loads.
- Transition jank: Page transitions that take time can show stale UI instead of spinners, with
useTransitioncontrolling the experience.
Key concurrent APIs: useTransition, useDeferredValue, Suspense for data, startTransition. Enabled by default in React 18 when you use createRoot instead of the legacy ReactDOM.render.
03 What is useTransition and useDeferredValue? When do you use each? ►
Concurrent Both APIs help with expensive UI updates but work differently.
// useTransition โ you control WHEN the update is non-urgent
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(e) {
setQuery(e.target.value); // urgent โ update input immediately
startTransition(() => {
setResults(expensiveFilter(e.target.value)); // non-urgent โ can be interrupted
});
}
return (
<>
<input value={query} onChange={handleSearch} />
{isPending ? <Spinner /> : <ResultsList results={results} />}
</>
);
}
// useDeferredValue โ you wrap a VALUE you don't own the setter for
function App({ searchResults }) {
const deferredResults = useDeferredValue(searchResults);
// deferredResults lags behind searchResults during heavy renders
return <HeavyList items={deferredResults} />;
}
Rule of thumb: Use useTransition when you own the state setter. Use useDeferredValue when the value comes from outside (props, context, third-party).
04 What are React Server Components (RSC)? How do they differ from SSR? ►
RSC React Server Components (introduced in React 18, adopted by Next.js App Router) are components that run only on the server โ they never ship to or execute on the client.
Key properties of RSCs:
- Zero JavaScript bundle impact โ their code is never sent to the client
- Can directly access databases, file systems, secrets, private APIs
- Cannot use hooks, event handlers, or browser APIs
- Can be async โ
async function Page() { const data = await db.query(...) } - Can import and render Client Components (with
'use client'directive) as children
RSC vs SSR:
- SSR runs on the server per-request and produces HTML, then ships all component JS to the client for hydration
- RSC runs on the server and its output is a serialised component tree โ never shipped as JS. Only the explicitly marked Client Components get bundled and hydrated
They are complementary: you typically use both in the same app โ RSCs for data fetching and static structure, Client Components for interactivity.
05 What is the stale closure problem in React hooks? How do you fix it? ►
Hooks A stale closure occurs when a hook callback (usually in useEffect, useCallback, or useMemo) “closes over” a value from a previous render and never sees the updated value.
// โ BUG: stale closure โ count is always 0 inside the interval
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always 0 โ stale!
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // empty dep array = effect only runs once, captures initial count
}
// โ
FIX 1: Add count to dependencies (re-creates interval each time)
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, [count]);
// โ
FIX 2: Functional updater โ no closure needed
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []); // safe โ doesn't read count at all
// โ
FIX 3: useRef to track latest value without re-running effect
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const id = setInterval(() => setCount(countRef.current + 1), 1000);
return () => clearInterval(id);
}, []);
06 What is hydration? What are the challenges of partial and selective hydration? ►
SSR Hydration is the process of attaching React’s JavaScript (event handlers, component state, effects) to server-rendered HTML that already exists in the DOM. The client receives static HTML for fast display, then React “hydrates” it to make it interactive.
Classic hydration challenges:
- The entire JS bundle must download and parse before any part of the page can be interactive
- The entire React tree must hydrate before any component becomes interactive โ a slow component blocks everything
- Hydration mismatches (server and client render different HTML) cause visible flicker and errors
React 18’s selective hydration: With Suspense boundaries in SSR, React can hydrate different parts of the page independently and in priority order. If the user clicks a button before its section hydrates, React prioritizes hydrating that section first.
Streaming SSR: React 18’s renderToPipeableStream streams HTML to the browser as each Suspense boundary resolves, eliminating the waterfall of waiting for all data before any HTML is sent.
07 What are compound components? Implement a real example. ►
Patterns The compound component pattern creates a group of components that work together, sharing implicit state through Context. Consumers get flexible composition without managing the shared state themselves.
const AccordionContext = React.createContext(null);
function Accordion({ children, defaultOpen = null }) {
const [openId, setOpenId] = useState(defaultOpen);
const toggle = useCallback(id => setOpenId(prev => prev === id ? null : id), []);
return (
<AccordionContext.Provider value={{ openId, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ id, title, children }) {
const { openId, toggle } = useContext(AccordionContext);
const isOpen = openId === id;
return (
<div>
<button onClick={() => toggle(id)}>{title}</button>
{isOpen && <div>{children}</div>}
</div>
);
}
Accordion.Item = AccordionItem; // attach as static property
// Consumer usage โ clean and declarative
<Accordion defaultOpen="q1">
<Accordion.Item id="q1" title="What is React?">React is...</Accordion.Item>
<Accordion.Item id="q2" title="What is JSX?">JSX is...</Accordion.Item>
</Accordion>
Real-world examples: Headless UI (Disclosure, Tabs), Radix UI primitives, React Bootstrap (Tabs/Tab), and Reach UI components all use this pattern extensively.
08 What is automatic batching in React 18? How does it differ from React 17? ►
React 18 Batching is React’s ability to group multiple state updates into a single re-render for better performance.
// React 17 โ only batched inside synchronous event handlers
document.addEventListener('click', () => {
setCount(c => c + 1); // both updates batched = 1 re-render โ
setFlag(f => !f);
});
// React 17 โ async code was NOT batched โ caused 2 separate re-renders
setTimeout(() => {
setCount(c => c + 1); // re-render 1 โ
setFlag(f => !f); // re-render 2 โ
}, 1000);
// React 18 โ ALL updates batched automatically, even in async code
setTimeout(() => {
setCount(c => c + 1); // batched = 1 re-render โ
setFlag(f => !f); // batched = 1 re-render โ
}, 1000);
// React 18 โ opt out when you need synchronous DOM reads
import { flushSync } from 'react-dom';
flushSync(() => setCount(c => c + 1)); // DOM updated immediately
const height = el.getBoundingClientRect().height; // safe to read
Automatic batching is available when using the new ReactDOM.createRoot() API. Apps still using the legacy ReactDOM.render() get the old React 17 batching behaviour.
09 What is useImperativeHandle? When would you use it over direct forwardRef? ►
Refs useImperativeHandle customises what is exposed when a parent component uses a ref on a child component. Instead of exposing the raw DOM node, you expose a controlled API.
const VideoPlayer = React.forwardRef(function(props, ref) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose these methods โ not the entire video DOM node
play: () => videoRef.current.play(),
pause: () => videoRef.current.pause(),
seekTo: (time) => { videoRef.current.currentTime = time; },
}));
return <video ref={videoRef} src={props.src} />;
});
// Parent can now do:
const playerRef = useRef(null);
playerRef.current.seekTo(30); // clean API, not playerRef.current.currentTime = 30
When to use:
- You want to expose a clean, minimal API to parents (encapsulation)
- You’re building a UI library component that should hide internal DOM details
- You need to expose methods that involve multiple internal DOM nodes or stateful logic
Use plain forwardRef when you simply want to expose the DOM node as-is (e.g., for a plain styled input). Use useImperativeHandle when you want to control or abstract what the parent can do.
10 What is React Query (TanStack Query)? Why is “server state” different from “client state”? ►
Data Fetching Server state is data that lives remotely (API, database). It has unique challenges: it can become outdated (stale), multiple components may need the same data, and mutations must sync back to the server. Client state is purely local UI state (modals open/closed, form values, theme).
Managing server state with useEffect + useState is painful โ you end up writing the same loading/error/caching/refetch logic over and over.
// TanStack Query replaces this pattern entirely
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['users', userId], // cache key
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // data is fresh for 5 minutes
});
// Mutations
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => queryClient.invalidateQueries(['users']), // refetch after update
});
What React Query provides automatically:
- Caching and deduplication โ multiple components requesting the same key share one request
- Background refetching โ data refreshes when the window regains focus
- Stale-while-revalidate โ show cached data instantly, fetch fresh data in background
- Pagination, infinite queries, optimistic updates, request cancellation
11 Compare Zustand, Jotai, and Redux. When would you choose each? ►
State Management
// Zustand โ minimal, store-based, ~1KB
const useCounterStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));
const count = useCounterStore(state => state.count);
// Jotai โ atomic, bottom-up, composable const countAtom = atom(0); const doubleAtom = atom(get => get(countAtom) * 2); // derived atom const [count, setCount] = useAtom(countAtom);
// Redux Toolkit โ verbose, structured, powerful devtools
const slice = createSlice({ name: 'counter', initialState: 0,
reducers: { increment: s => s + 1 }});
const count = useSelector(state => state.counter);
Choose based on:
- Zustand โ small-to-medium apps, minimal boilerplate, pragmatic, great for replacing prop drilling and Context
- Jotai โ apps with many small, independent pieces of state; derived state; fine-grained re-renders by atom subscription
- Redux Toolkit โ large teams, enterprise apps, need for strict conventions, advanced DevTools (time-travel, action logging), RTK Query for API state
12 What is micro-frontend architecture? How does Module Federation work with React? ►
Architecture Micro-frontends decompose a large frontend into independently deployed units owned by different teams โ each a full vertical slice with its own React app, build pipeline, and deployment.
Webpack Module Federation (MF) is the most common approach for React:
// Host app webpack.config.js
new ModuleFederationPlugin({
remotes: {
checkout: 'checkout@https://checkout.myapp.com/remoteEntry.js',
}
})
// In host app โ dynamically loaded from checkout.myapp.com
const CheckoutWidget = React.lazy(() => import('checkout/CheckoutWidget'));
// Checkout app webpack.config.js (separate deployment)
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: { './CheckoutWidget': './src/CheckoutWidget' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
})
Key challenges:
- Shared dependencies โ React must be a singleton across all micro-frontends to avoid multiple React instances
- Shared state โ micro-frontends communicate via custom events, shared URL state, or a shared context
- Styling conflicts โ use CSS-in-JS, Shadow DOM, or BEM naming conventions
- Deployment coordination โ version mismatches between host and remote apps
13 What is the React DevTools Profiler? How do you interpret a flamegraph? ►
Dev Tools The React DevTools Profiler records timing information about component renders. It is one of the most powerful tools for finding performance bottlenecks.
How to use it:
- Open React DevTools โ Profiler tab
- Click “Record”, perform the interaction you want to profile, click “Stop”
- Each bar in the flamegraph represents a component; width = render duration
- Colour coding: grey = didn’t render, other colours = rendered (darker = slower)
Reading the flamegraph:
- Wide bars are slow โ investigate those first
- Unexpected re-renders โ components that rendered when they shouldn’t have (often signals missing
React.memoor unstable prop references) - Use the “Why did this render?” feature to see exactly which prop/state triggered the render
Programmatic profiling: The <Profiler> component can log render timings to a monitoring service in production:
function onRenderCallback(id, phase, actualDuration) {
if (actualDuration > 16) logToMonitoring({ id, phase, actualDuration });
}
<Profiler id="Dashboard" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
14 Explain React 18 vs React 19 โ what are the major new features? ►
React 19
React 18 (2022) introduced:
- Automatic batching for all state updates
- Concurrent features:
useTransition,useDeferredValue,startTransition - New root API:
ReactDOM.createRoot() - Streaming SSR with
renderToPipeableStreamand selective hydration useIdfor stable IDs in SSR,useSyncExternalStorefor external stores
React 19 (2024) introduced:
- Actions โ async functions that automatically manage pending states, errors, and transitions. Work with HTML forms natively.
- useOptimistic โ show expected optimistic UI updates before the server confirms them; automatically rolls back on error.
- use() hook โ read a Promise or Context value during render; integrates with Suspense. Can be called conditionally.
- Server Actions (with framework support) โ call server functions directly from client components.
- ref as a prop โ no longer need
forwardRef; refs can be passed as a regular prop in React 19. - Document metadata โ
<title>,<meta>,<link>tags now work correctly when rendered inside components.
15 What is the islands architecture and how does it relate to React? ►
Architecture The islands architecture renders the vast majority of a page as static HTML on the server, with isolated “islands” of client-side JavaScript only where interactivity is needed. Everything outside the islands is zero-JS, static HTML.
// Astro example โ most of the page is static
---
import StaticHeader from './Header.astro'; // zero JS โ just HTML
import ReactCounter from './Counter.jsx'; // interactive island
---
<html>
<StaticHeader /> // static HTML, ~0KB JS
<main>
<p>This is static content.</p>
<ReactCounter client:load /> // React island โ hydrated on load
<ReactChart client:idle /> // hydrated when browser is idle
</main>
</html>
Why it matters for React: Traditional React apps ship all component code to the client. Islands architecture is the extreme version of partial hydration โ only the interactive parts ship as JavaScript. This can reduce JS bundle size by 90%+ for mostly-static sites like blogs, documentation, or marketing pages.
Frameworks: Astro (native islands, can embed React islands), Fresh (Deno-based). Next.js App Router with Server Components achieves similar results โ RSCs are server-only, only Client Components ship JS.
16 What are the SOLID principles applied to React components? ►
Architecture
- S โ Single Responsibility: A component should do one thing. A
UserProfilePageshould not also handle data fetching, auth, analytics โ extract hooks and sub-components. - O โ Open/Closed: Components should be open for extension but closed for modification. Use composition and props (especially
childrenor render props) to extend behaviour without editing the source. - L โ Liskov Substitution: A specialised component should be usable wherever the base component is expected.
PrimaryButtonshould work anywhereButtonworks. - I โ Interface Segregation: Components should not be forced to depend on props they don’t use. Split large prop interfaces into focused, smaller ones. Avoid “god components” with 20+ props.
- D โ Dependency Inversion: Depend on abstractions, not concretions. Pass dependencies (data fetchers, formatters) as props or inject them via Context rather than importing directly โ makes components easier to test.
In practice: keep components small and focused, prefer composition over inheritance, and use hooks to abstract side effects.
17 How does React handle form submissions with Actions in React 19? ►
React 19 React 19 introduced Actions โ a pattern for handling async form submissions with automatic pending state, error handling, and optimistic updates.
// React 19 โ form action with useActionState
import { useActionState } from 'react';
async function submitForm(prevState, formData) {
const name = formData.get('name');
try {
await api.updateProfile({ name });
return { success: true, message: 'Profile updated!' };
} catch (err) {
return { success: false, message: err.message };
}
}
function ProfileForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input name="name" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
This replaces the verbose React 18 pattern of useState + useTransition + manual error handling. React 19 also adds useFormStatus to read pending state inside a form without prop drilling.
18 What is useOptimistic in React 19 and how does it improve UX? ►
React 19 useOptimistic enables optimistic UI updates โ showing the expected result of an action immediately while the async operation is in flight, then reverting automatically if it fails.
function MessageList({ initialMessages }) {
const [messages, setMessages] = useState(initialMessages);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { ...newMessage, sending: true }]
);
async function sendMessage(text) {
// Immediately show message with "sending" indicator
addOptimisticMessage({ id: Date.now(), text });
// Actual async send
const saved = await api.sendMessage(text);
// Update real state โ optimistic UI merges with real data
setMessages(prev => [...prev, saved]);
}
return (
<ul>
{optimisticMessages.map(msg => (
<li key={msg.id} style={{ opacity: msg.sending ? 0.6 : 1 }}>
{msg.text} {msg.sending && '(sending...)'}
</li>
))}
</ul>
);
}
If the async operation fails, React automatically reverts optimisticMessages back to the real messages state. This creates instant, responsive UIs even on slow networks โ a pattern that was previously complex to implement correctly.
19 How would you architect a large-scale React application from scratch? ►
Architecture A scalable React architecture for a large enterprise application:
Technology decisions:
- Framework: Next.js (App Router) for SSR, routing, and RSC support
- Language: TypeScript for type safety across the codebase
- State: TanStack Query for server state, Zustand for client state, URL for shareable state
- Styling: Tailwind CSS + a component library (shadcn/ui or Radix UI)
- Testing: Vitest (unit), React Testing Library (integration), Playwright (E2E)
Folder structure (feature-based):
src/
features/ # Feature modules (auth, orders, users)
auth/
components/ # Components for this feature only
hooks/ # Custom hooks for this feature
api/ # API calls for this feature
types.ts
shared/ # Shared across features
components/ # UI primitives (Button, Input, Modal)
hooks/ # useDebouce, useLocalStorage, etc.
utils/ # formatters, validators
app/ # Next.js App Router pages and layouts
Key principles: Colocate feature code, enforce boundaries (features don’t import from each other directly โ use the shared layer), keep components small (under 200 lines), separate UI from business logic via hooks.
20 How do you handle accessibility (a11y) in React applications? ►
Accessibility Accessibility is a first-class concern in production React apps, not an afterthought. Key practices:
Semantic HTML:
// โ Bad โ div soup loses semantic meaning
<div onClick={handleSubmit}>Submit</div>
// โ
Good โ native elements have built-in keyboard support and ARIA roles
<button type="submit" onClick={handleSubmit}>Submit</button>
ARIA attributes when native HTML isn’t enough:
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title"> <h2 id="dialog-title">Confirm Delete</h2> </div>
Focus management:
// Move focus to modal on open, restore on close
useEffect(() => {
if (isOpen) {
firstFocusableRef.current?.focus();
return () => previousFocusRef.current?.focus();
}
}, [isOpen]);
Tools and libraries:
eslint-plugin-jsx-a11yโ catches accessibility issues at lint time@axe-core/reactโ runtime a11y checks in development- Radix UI / Headless UI โ fully accessible unstyled primitives for complex components (modals, menus, comboboxes)
- Testing:
@testing-library/reactencourages accessible queries (getByRole,getByLabelText)
21 What is tree shaking and how do you ensure your React app is well tree-shaken? ►
Performance Tree shaking is dead code elimination โ bundlers (Webpack, Rollup, Vite) statically analyse ES module imports and remove code that is never used. The result: a smaller bundle.
Requirements for tree shaking to work:
- Code must use ES modules (
import/export) โ CommonJS (require) is not statically analysable - Libraries must publish ES module format (check
moduleorexportsfield in package.json) - No side effects at module level (check
"sideEffects": falsein library’s package.json)
// โ Imports entire lodash bundle (~530KB)
import _ from 'lodash';
const sorted = _.sortBy(arr, 'name');
// โ
Imports only the sortBy module (tree-shakeable)
import sortBy from 'lodash/sortBy';
// or with lodash-es (full ES module version)
import { sortBy } from 'lodash-es';
Analysis tools:
vite-bundle-visualizer/webpack-bundle-analyzerโ visualise what’s in your bundlebundlephobia.comโ check library size and tree-shakeability before adding a dependency- Source maps with
source-map-explorerโ see exactly which files contribute what to bundle size
📝 Knowledge Check
These questions mirror real senior-level interview scenarios. Think carefully before answering.