Advanced React interviews are less about reciting hook definitions and more about reasoning from component identity, render and commit phases, scheduling, and measured performance. These nine questions cover the trade-offs a senior developer should be able to explain without relying on outdated React folklore.
Table of Contents
- React Internals Questions
- React Hooks Questions
- Performance Optimization Questions
- Error Handling Questions
- State Management Questions
- Quick Reference
React Internals Questions
These questions test your understanding of how React works under the hood.
How does React reconciliation work?
Reconciliation is the process React uses to compare the latest element output with the previous render. It decides whether to preserve an existing component instance and host node or replace it. This is a heuristic process, not a search for a globally minimal DOM edit script.
The first heuristic: elements of different types produce entirely different trees. If a <div> becomes a <span>, React tears down the old tree completely and builds a new one—it doesn't try to morph one into the other.
The second heuristic: keys help React match sibling items across renders. Without an explicit key, React falls back to position, which can associate local state and DOM with the wrong item after an insertion, deletion, or reorder.
// Positional identity: unsafe when the collection can change order
{items.map(item => <ListItem data={item} />)}
// Stable identity among siblings
{items.map(item => <ListItem key={item.id} data={item} />)}The key insight is identity. A different component type at the same position resets that subtree. A stable key lets React track an item among its siblings, while changing a key deliberately resets its state. Keys are local to a parent; they are not global IDs.
An array index is acceptable only when the list is truly static. In a mutable list, the same index may refer to a different record on the next render, so React can preserve input state or a DOM node for the wrong record. A key should come from stable data rather than be generated during render.
What is React Fiber and why was it introduced?
Fiber is the internal reconciler architecture introduced in React 16. It represents the work associated with component trees as units React can schedule. In concurrent rendering, React may interrupt or abandon an in-progress render before committing it.
The important distinction is between the render phase, where React computes the next tree, and the commit phase, where it applies the result. Concurrent work can make rendering interruptible, but a commit remains a coordinated update. Application code should rely on public APIs and purity rules rather than Fiber's private fields.
This architecture underpins Suspense and Transitions. A Transition marks non-urgent state updates as non-blocking work, allowing an urgent update such as controlled input state to remain responsive.
// Keep controlled input state outside the Transition
import { startTransition } from 'react';
function handleSearch(query) {
setInputValue(query);
// Mark the results update as a non-blocking Transition
startTransition(() => {
setSearchResults(filterResults(query));
});
}The interview-level mental model is that rendering may be restarted, so render logic must stay pure. Do not describe Fiber scheduling details as stable public guarantees.
What is the Virtual DOM and why does React use it?
“Virtual DOM” is shorthand for React's in-memory element and fiber representations. It is not inherently faster than carefully written direct DOM code. Its main value is a declarative component model: developers describe the desired UI, and React coordinates rendering and commits.
Direct DOM manipulation is fast for individual operations, but synchronizing a large UI with application state is difficult to maintain. React centralizes that synchronization and can skip host changes when the committed output is already correct.
React's approach: let developers describe what the UI should look like (declarative), and React figures out how to update the DOM (imperative). The Virtual DOM is the intermediate representation that makes this possible:
State update → Render/reconcile → Commit host changes
The key insight is not an automatic performance win. React provides a composable declarative model and controls when a completed render is committed.
Batching and reconciliation are related but distinct. Batching groups state updates to avoid unnecessary renders. A commit can still contain multiple DOM mutations; React does not promise to apply every update as one DOM operation.
React Hooks Questions
These questions test your understanding of React hooks and when to use each one.
What is the difference between useCallback and useMemo?
Both hooks memoize something, but what they memoize and why you'd use them are different.
useMemo memoizes the result of calling a function. Use it when you have an expensive computation that you don't want to repeat on every render:
// Without useMemo: filterItems runs on every render
const filteredItems = filterItems(items, query);
// With useMemo: React can reuse the last result while dependencies match
const filteredItems = useMemo(
() => filterItems(items, query),
[items, query]
);useCallback memoizes the function itself. Use it when you pass callbacks to optimized child components that rely on reference equality:
// Without useCallback: new function reference on every render
// Breaks React.memo optimization on ExpensiveList
<ExpensiveList onItemClick={(id) => handleClick(id)} />
// With useCallback: same function reference if dependencies unchanged
const handleClick = useCallback((id) => {
selectItem(id);
}, [selectItem]);
<ExpensiveList onItemClick={handleClick} />Here's the critical insight: useCallback and useMemo are performance optimizations, not semantic guarantees. React may discard a memo cache, so correctness must not depend on it. They also add dependency management and can be defeated by a single value that is always new.
Profile the production build before adding manual memoization. useMemo can help with a measurably expensive calculation or a stable value passed across a memoized boundary; useCallback can stabilize a callback in the same situation. When React Compiler is enabled, it automatically handles many component, value, and function memoization cases, although the hooks remain available for precise control.
When should you use useLayoutEffect instead of useEffect?
Both hooks run after render, but at different points in the browser's paint cycle.
useEffect does not block the browser from painting. Depending on how an update was triggered, React and the browser may schedule it around paint; code should not use it for layout-critical work.
Commit → browser may paint → useEffect
useLayoutEffect runs after React has changed the DOM and before the browser repaints. React processes its state updates before allowing that repaint:
Commit DOM → useLayoutEffect → Browser Paint
Use useLayoutEffect when you need to read layout and synchronously re-render to prevent a visual flicker:
function Tooltip({ targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
// useLayoutEffect prevents flicker when positioning
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPosition({ top: rect.bottom, left: rect.left });
}, [targetRef]);
return <div style={position}>Tooltip content</div>;
}If you used useEffect here, users would see the tooltip appear at (0, 0), then jump to the correct position—an ugly flicker.
However, useLayoutEffect blocks painting, so slow work there hurts responsiveness. Prefer useEffect unless a layout measurement or visual correction must happen before repaint. Effects run only on the client, so server-rendered components must not depend on either Effect for their initial HTML.
When and how should you create custom hooks?
Custom hooks let you extract stateful logic into reusable functions. The key word is stateful—if you're just extracting pure computation, a regular function works fine. Custom hooks are for when you need to use other hooks.
A good custom hook encapsulates a complete behavior:
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
try {
const saved = window.localStorage.getItem(key);
return saved === null ? initialValue : JSON.parse(saved);
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
// Report or expose quota/security failures in production code.
}
}, [key, value]);
return [value, setValue];
}
// Usage - clean and reusable
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
// ...
}The mental model: a custom hook shares logic, not state itself. Each call is independent unless the hook connects to a shared external source. It follows the same Rules of Hooks as a component: call hooks at the top level and use the use naming convention so linting can recognize it.
The example is intentionally small. A production storage hook should also define behavior for key changes, serialization failures, hydration, and cross-tab storage events rather than silently assuming storage is always available.
Strong custom hooks follow these patterns:
- They have a single, clear purpose (like
useLocalStorage, notuseEverything) - They return an array or object with a consistent interface
- They handle cleanup properly in useEffect
- They accept configuration through parameters but have sensible defaults
Performance Optimization Questions
These questions test your ability to identify and fix performance issues in React applications.
What causes unnecessary re-renders and how do you prevent them?
Rendering is normal React work, not a bug by itself. Optimize only when profiling shows that a particular interaction is slow.
By default, when a component renders, React also renders component children it calls. React.memo() can skip a child render when its props are unchanged, but it is useful only at a measured boundary:
// Without memo: re-renders whenever parent re-renders
function UserCard({ user }) {
return <div>{user.name}</div>;
}
// With memo: React can skip rendering while props compare equal
const UserCard = React.memo(function UserCard({ user }) {
return <div>{user.name}</div>;
});New object, array, and function references matter when they cross a memoized boundary or appear in a dependency list. A static value can simply live outside the component:
// Problem: new style object created every render
<UserCard style={{ padding: 10 }} />
// Module-level constant: stable without a hook
const cardStyle = { padding: 10 };
<UserCard style={cardStyle} />When a provider receives a different value by Object.is, React re-renders the consumers that read that context. A broad context can therefore do more work than necessary:
// Problem: every AuthContext consumer re-renders when theme changes
const AppContext = React.createContext({ user: null, theme: 'light' });
// Solution: split into separate contexts
const UserContext = React.createContext(null);
const ThemeContext = React.createContext('light');Start by keeping transient state close to where it is used and removing Effects that produce update chains. Then use React DevTools Profiler to locate expensive commits. Split context, stabilize props, or add memoization only where the evidence supports it. React Compiler can automate much of this memoization when the project enables it.
Error Handling Questions
These questions test your ability to build robust React applications that handle errors gracefully.
How do Error Boundaries work in React?
Error Boundaries are class components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the whole app.
A component becomes an Error Boundary by implementing one or both of these lifecycle methods:
class ErrorBoundary extends React.Component {
state = { hasError: false };
// Called during render to update state
static getDerivedStateFromError(error) {
return { hasError: true };
}
// Called after render for logging
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}The key limitation: Error Boundaries don't catch errors in event handlers, arbitrary callbacks such as setTimeout, errors while producing server-rendered output, or errors thrown in the boundary itself. React can route some errors thrown inside a Transition to an Error Boundary, so “no async errors” is too broad a rule.
For event handler errors, you need traditional try/catch:
function Button() {
const handleClick = () => {
try {
doSomethingRisky();
} catch (error) {
// Handle error manually
setState({ error });
}
};
return <button onClick={handleClick}>Click me</button>;
}A practical production pattern is placing boundaries around independently recoverable regions and logging failures through componentDidCatch. React 19 root options such as onCaughtError, onUncaughtError, and onRecoverableError provide additional reporting hooks; they do not replace fallback UI boundaries.
State Management Questions
These questions test your understanding of state management patterns and trade-offs.
How does Context API work and when should you avoid it?
Context provides a way to pass data through the component tree without prop drilling. It's designed for data that can be considered "global" for a component tree—things like current user, theme, or locale.
Creating and using context is straightforward:
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<MainContent />
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme } = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}When the provider receives a different value, React updates components that consume that context. Cost depends on the number and weight of those consumers, not simply on how high the provider sits in the JSX tree:
// Problem: every consumer re-renders on every mouse move
const MouseContext = React.createContext({ x: 0, y: 0 });
// Every MouseContext consumer observes every position update
function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<MouseContext.Provider value={position}>
<MousePosition />
</MouseContext.Provider>
);
}When to use Context: data that many descendants need, such as theme, locale, authenticated user, or a reducer's dispatch function.
When to reconsider one broad Context: a measured hot path where many expensive consumers update for data they do not use. Options include splitting contexts, separating state from a stable dispatch context, keeping state local, composing children, or using an external store with selective subscriptions. Frequently changing data is not automatically forbidden in Context; granularity and consumer cost determine the trade-off.
Quick Reference
| Topic | Key Points |
|---|---|
| Reconciliation | Type, position, and keys determine identity; no minimum-edit guarantee |
| Fiber | Internal scheduling architecture behind concurrent rendering capabilities |
| Virtual DOM | In-memory representation supporting a declarative UI model |
| useMemo | Caches calculation results as a performance optimization |
| useCallback | Caches function definitions for measured memoization boundaries |
| useLayoutEffect | Runs before paint; use for DOM measurements to prevent flicker |
| Custom Hooks | Extract stateful logic; must start with use, follow hook rules |
| Re-renders | Profile first; localize state and optimize proven hot paths |
| Error Boundaries | Catch descendant render/lifecycle errors and provide fallback UI |
| Context | Consumers update with provider value; choose useful granularity |
Official Sources
- Preserving and resetting state
- Rendering lists and choosing keys
startTransitionuseMemouseCallbackuseLayoutEffectmemo- React Compiler
- Error Boundaries with
Component createRooterror-reporting optionscreateContext
Related Articles
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- React Hooks Interview Guide - Master useState, useEffect, and custom hooks
- React 19 Interview Guide - New features like Actions, use() hook, and Server Components
- JavaScript Event Loop Interview Guide - How async JavaScript really works under the hood
- Top 5 React Interview Mistakes - Common hooks and rendering mistakes to avoid
Frequently Asked Questions
What is React Fiber and why was it introduced?
React Fiber is the internal reconciler architecture introduced in React 16. It represents rendering work as units that React can schedule and, for concurrent updates, interrupt or abandon. Fiber underpins capabilities such as Suspense and Transitions, but its implementation details are not a public API contract.
How does React reconciliation work?
Reconciliation compares the latest element output with the previous render and decides which component instances and host nodes can be reused. Element type, position, and keys define identity. The algorithm uses heuristics; it does not promise a globally minimal set of DOM operations.
What is the difference between useCallback and useMemo?
useMemo caches a calculation result, while useCallback caches a function definition. Both are performance optimizations rather than semantic guarantees, so code must remain correct without their caches. Profile first; when React Compiler is enabled, it can provide much of this memoization automatically.
When should you use useLayoutEffect instead of useEffect?
useLayoutEffect fires synchronously after DOM mutations but before the browser paints. Use it when you need to read layout from the DOM and synchronously re-render, such as measuring element dimensions or preventing visual flickers. useEffect is preferred for most cases as it doesn't block the browser from painting.
How do Error Boundaries work in React?
Error Boundaries catch errors thrown while rendering descendant components and in their lifecycle methods, then show fallback UI. A class boundary uses getDerivedStateFromError() and optionally componentDidCatch(). It does not catch its own errors, event-handler errors, arbitrary asynchronous callbacks, or errors while producing server-rendered output.
What causes unnecessary re-renders and how do you prevent them?
A parent render normally renders its children, and a changed context value renders its consumers. First confirm a real bottleneck with React DevTools Profiler. Then keep state local, remove update-producing Effects, stabilize props at measured memoization boundaries, split contexts where useful, or use React.memo. React Compiler can automate many memoization cases.
