React Interview: State Management, Performance & Modern React
The senior round — Context and when it hurts, choosing a state library, diagnosing wasted re-renders with the Profiler, virtualization, error boundaries and Suspense, plus Server Components and what changed in React 18 and 19.
4 sections · ~38 min · 5-question quiz (pass ≥ 70%)
1Context, Prop Drilling, and Choosing a State Solution
Context solves prop drilling, not state management. It is a transport mechanism — it does not add caching, batching, or selective subscriptions.
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState("light");
const value = useMemo(() => ({ theme, setTheme }), [theme]); // see below
return (
<ThemeContext.Provider value={value}>
<Layout />
</ThemeContext.Provider>
);
}
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
}
That guard-throwing custom hook is the idiomatic pattern and worth showing.
The performance trap: every consumer re-renders whenever the context value's reference changes, regardless of which part of it they read. value={{ theme, setTheme }} creates a new object every render, so all consumers re-render on any parent render. Fixes:
useMemothe value (as above).- Split contexts — one for rarely-changing data, one for frequently-changing data. Or split state and dispatch into two contexts, since dispatch is stable.
- For genuinely high-frequency updates, use a store with selector-based subscriptions instead.
Choosing a solution — the answer interviewers want is a decision tree, not a favourite library:
| Situation | Answer |
|---|---|
| Used by one component | useState |
| Complex transitions in one component | useReducer |
| Shared by a subtree, changes rarely (theme, user, locale) | Context |
| Server data (lists, details, mutations) | React Query / SWR — caching, revalidation, dedupe |
| Complex shared client state across routes | Zustand / Redux Toolkit / Jotai |
| Belongs in the URL (filters, tabs, page) | Query params — shareable and back-button friendly |
The single strongest point you can make here: most "global state" in real apps is server cache, and it should not live in Redux at all. Separating server state from client state, and naming React Query as the tool for the former, is a senior-level answer.
2Diagnosing and Fixing Wasted Renders
Measure first. The React DevTools Profiler records a commit and shows which components rendered and why ("Why did this render?" must be enabled in settings). Guessing at performance is the mistake; the tool is the answer.
Why a component re-renders — the complete list:
- Its own state changed.
- Its parent re-rendered (default behaviour — props do not need to change).
- A context it consumes changed.
- Its
keychanged (that is a remount, not a re-render).
Fix in this order:
1. Move state down. If only a leaf uses a piece of state, put it in that leaf. This is free and beats any memoization.
2. Lift content up / pass children. A parent whose state changes does not re-render children it received as props:
function Wrapper({ children }) {
const [open, setOpen] = useState(false);
return <div>{children}</div>; // children were created by the PARENT;
} // toggling open does not re-render them
3. Then memoize — React.memo on the child, useCallback/useMemo on the props it receives. All three are needed together; memoizing the child while passing a fresh inline arrow function does nothing.
const Row = React.memo(function Row({ item, onSelect }) { ... });
const handleSelect = useCallback((id) => setSelected(id), []);
<Row item={item} onSelect={handleSelect} />
Beyond re-renders:
- Long lists → virtualization: render only the visible window (
react-window,@tanstack/react-virtual). This is the expected answer for "10,000 rows". - Big bundles →
React.lazy+Suspensefor route-level code splitting. - Expensive derived data →
useMemo, but confirm it is actually expensive first. - Images → lazy loading, correct sizing, modern formats.
- Blocking updates →
useTransitionmarks an update non-urgent so typing stays responsive while an expensive list re-filters.
Reframe the question when asked "how do you optimise React?" Start with "I'd profile to find what's actually slow", then talk about network and bundle size before micro-optimising renders. Most real React slowness is data fetching and payload size, not reconciliation.
3Error Boundaries, Suspense, and Data Fetching Patterns
Error boundaries catch render-time errors in their subtree and show a fallback instead of unmounting the whole app. They must be class components (there is no hook equivalent yet) — a fact interviewers like to check:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error, info) { logToService(error, info); }
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
They do NOT catch: errors in event handlers, async code (setTimeout, promise rejections), server-side rendering, or errors thrown inside the boundary itself. Handle those with try/catch where they happen. Knowing the exclusions is the real question.
Suspense lets a component declare "I'm not ready" and show a fallback while it waits:
<Suspense fallback={<Skeleton />}>
<LazyDashboard />
</Suspense>
Used for React.lazy code splitting, and — with a Suspense-enabled data layer or Server Components — for data too.
Data fetching patterns, worst to best:
useEffect+fetch— the baseline. Waterfalls, no cache, manual loading/error/race handling. Fine for an interview if you handle cleanup.- A
useFetchcustom hook — same mechanics, deduplicated code. - React Query / SWR — caching, background revalidation, request deduplication, retries, pagination, optimistic updates. The right production answer.
- Server Components / route loaders — fetch on the server, ship no fetching code to the client at all.
The waterfall problem: a parent fetches, renders a child, the child then fetches. Two sequential round trips. Fix by hoisting both fetches to the parent and running them in parallel (Promise.all), or by fetching at the route level.
Optimistic updates — apply the change locally, fire the request, roll back on failure. Expect to be asked how you would implement a "like" button that feels instant.
4Modern React: Server Components, React 18 & 19
Server Components (RSC) run on the server only. Their code is never shipped to the browser; they can query a database directly and render to a serialised tree.
// Server Component (default in the Next.js App Router)
export default async function Page() {
const users = await db.user.findMany(); // no useEffect, no API route
return <UserList users={users} />;
}
Client Components are opted in with "use client" and are what you need for state, effects, refs, event handlers, and browser APIs.
The rules that get asked:
- A Server Component can render a Client Component; the reverse only works by passing the server-rendered element through as
children. - Props crossing the server→client boundary must be serialisable — no functions, no class instances, no Dates in some setups.
- Server Components reduce bundle size and remove client-side data waterfalls; they cannot hold state.
- RSC is not the same as SSR: SSR renders your client components to HTML on each request; RSC components never hydrate at all.
React 18 additions:
- Automatic batching everywhere, including promises and timeouts.
- Concurrent rendering — React can interrupt and resume a render.
useTransition/startTransitionto mark low-priority updates;useDeferredValuefor a lagging copy of a value.- Strict Mode double-invokes effects in development to surface missing cleanup.
createRootreplacedReactDOM.render.
React 19 additions:
- Actions —
useActionState,useFormStatus, anduseOptimisticbuild pending/error/optimistic handling into form submission. - The React Compiler auto-memoizes, making most manual
useMemo/useCallbackunnecessary. use()— read a promise or context conditionally during render.refis now a regular prop;forwardRefis no longer required for new code.- Document metadata (
<title>,<meta>) can be rendered directly from components.
How to use this in an interview: you are not expected to have shipped all of it. Being able to say what problem each feature solves — "the compiler exists because manual memoization is error-prone and easy to get wrong" — is what demonstrates real understanding.