React's reconciliation algorithm achieves O(n) complexity instead of the theoretical O(n³) required for tree diffing—a 1,000,000x performance improvement for a tree with 1,000 elements. How? Through two heuristics that every senior React developer should understand cold.
These nine questions test deep understanding rather than API memorization. They consistently separate senior React developers from those who've simply used the framework. For each question, I'll show you what a weak answer looks like, then walk through what interviewers actually want to hear.
Question 1: How Does React Reconciliation Work?
This question immediately reveals whether a candidate understands React's core mechanism.
Weak answer: "React compares the Virtual DOM with the real DOM and updates what changed."
Strong answer: Reconciliation is React's algorithm for determining what changes need to be made to the actual DOM. When state or props change, React creates a new Virtual DOM tree and compares it with the previous one. This comparison uses two key heuristics to achieve O(n) performance instead of O(n³).
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 identify which items in a list have changed, moved, or been removed. Without keys, React has to re-render the entire list when one item changes.
// Without keys - React can't tell which item changed
{items.map(item => <ListItem data={item} />)}
// With keys - React knows exactly what moved or changed
{items.map(item => <ListItem key={item.id} data={item} />)}The key insight is that reconciliation is a heuristic—it trades perfect accuracy for speed. React assumes that cross-component moves are rare, so it doesn't try to detect them. Understanding this explains why reorganizing your component structure can cause unexpected unmount/remount cycles.
What interviewers are looking for: Knowledge of the diffing heuristics, understanding of why keys matter (not just that they do), and awareness that reconciliation is about performance trade-offs.
Common follow-up: "Why is using array index as a key problematic?" Strong candidates explain that indices don't provide stable identity—when items are reordered or removed, the index of remaining items changes, causing React to remount components that didn't actually change.
Question 2: What Is React Fiber and Why Was It Introduced?
This question distinguishes developers who've kept up with React's evolution from those still running on mental models from 2015.
Weak answer: "Fiber is the new reconciliation algorithm."
Strong answer: Fiber is a complete reimplementation of React's core algorithm, introduced in React 16. The old reconciler (called "Stack") processed updates synchronously in one big chunk—if a complex component tree took 100ms to render, the main thread was blocked for 100ms, causing dropped frames and janky animations.
Fiber introduced incremental rendering by breaking work into small units called "fibers" that can be paused, aborted, or resumed. Think of the difference like this: Stack is like reading an entire book in one sitting, while Fiber is like reading with bookmarks—you can stop at any chapter, do something else, and pick up where you left off.
This architecture enables several important capabilities. First, React can now prioritize updates. Typing in an input field should feel instant, while updating a large data table can be deferred. Second, Fiber is the foundation for Concurrent Mode and features like Suspense, Transitions, and the startTransition API in React 18.
// React 18's startTransition lets you mark updates as low priority
import { startTransition } from 'react';
function handleSearch(query) {
// High priority: update what the user typed
setInputValue(query);
// Low priority: update search results (can be interrupted)
startTransition(() => {
setSearchResults(filterResults(query));
});
}The key mental model shift: React is no longer just rendering your UI—it's scheduling work and deciding when to do it based on priority and available time.
What interviewers are looking for: Understanding that Fiber enables interruptible rendering and priority-based scheduling. Bonus points for connecting Fiber to concurrent features in React 18.
Question 3: useCallback vs useMemo - When Do You Use Each?
This question catches developers who've memorized the hooks but don't understand when to apply them.
Weak answer: "useMemo is for values, useCallback is for functions. You use them to prevent re-renders."
Strong answer: 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: filterItems only runs when items or query change
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 most developers miss: useCallback and useMemo aren't always beneficial. They have overhead—React needs to store the cached value and compare dependencies. If the computation is cheap or the component re-renders rarely anyway, memoization adds complexity without benefit.
The rule of thumb: use useMemo for computationally expensive operations, use useCallback for callbacks passed to memoized children, and don't use either "just in case."
What interviewers are looking for: Understanding that useCallback memoizes the function reference, not its execution. Awareness that memoization isn't free and shouldn't be applied blindly.
Common follow-up: "Can you implement useMemo using useRef?" Strong candidates know that useMemo is essentially a specialized use of useRef with dependency comparison—you could store the cached value and dependencies in a ref and compare manually.
Question 4: What Causes Unnecessary Re-renders and How Do You Prevent Them?
This question tests practical performance optimization knowledge.
Weak answer: "Use React.memo everywhere."
Strong answer: Re-renders happen for three main reasons, and each has a specific solution.
First, parent re-renders cause child re-renders by default. When a parent's state changes, all children re-render even if their props haven't changed. The fix is React.memo():
// Without memo: re-renders whenever parent re-renders
function UserCard({ user }) {
return <div>{user.name}</div>;
}
// With memo: only re-renders if user prop actually changed
const UserCard = React.memo(function UserCard({ user }) {
return <div>{user.name}</div>;
});Second, creating new object/array references breaks memoization. Even with React.memo(), if you pass a new object on every render, the shallow comparison fails:
// Problem: new style object created every render
<UserCard style={{ padding: 10 }} />
// Solution: memoize the object
const cardStyle = useMemo(() => ({ padding: 10 }), []);
<UserCard style={cardStyle} />Third, context changes re-render all consumers. When any part of context value changes, every component using that context re-renders:
// 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');The React DevTools Profiler is essential for diagnosing re-render issues. It shows exactly which components rendered, why they rendered, and how long each render took.
What interviewers are looking for: Systematic understanding of why re-renders happen, not just how to prevent them. Knowledge of profiling tools.
Question 5: useEffect vs useLayoutEffect - When Do You Use Each?
This question tests understanding of React's lifecycle timing.
Weak answer: "useLayoutEffect is like componentDidMount and useEffect is for side effects."
Strong answer: Both hooks run after render, but at different points in the browser's paint cycle.
useEffect runs asynchronously after the browser has painted. This means users see the initial render, then the effect runs:
Render → Browser Paint → useEffect runs
useLayoutEffect runs synchronously after DOM mutations but before the browser paints:
Render → useLayoutEffect runs → 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 it can make your app feel slower if the effect is slow. The rule: prefer useEffect unless you're measuring layout or need to prevent visual inconsistency.
What interviewers are looking for: Understanding of the timing difference relative to browser paint. Practical examples of when useLayoutEffect is necessary.
Question 6: How Do Error Boundaries Work?
This question reveals whether you've built production applications that need graceful error handling.
Weak answer: "Error boundaries catch errors and show a fallback."
Strong answer: 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, async code (like setTimeout or fetch), server-side rendering, or errors thrown in the Error Boundary itself.
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 pattern that's served me well is creating specialized Error Boundaries for different parts of the app—one for the sidebar, one for the main content, one for widgets—so a crash in one section doesn't take down the entire page.
What interviewers are looking for: Knowledge of what Error Boundaries catch and don't catch. Practical experience with error handling patterns.
Question 7: Custom Hooks - When and How to Create Them?
This question tests code organization and reusability instincts.
Weak answer: "Custom hooks extract logic from components."
Strong answer: 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(() => {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [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 is a function that calls other hooks. It follows the same rules as hooks in components—must be called at the top level, must start with use, can't be called conditionally.
Strong custom hooks follow these patterns. They have a single, clear purpose (like useLocalStorage, not useEverything). 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.
What interviewers are looking for: Ability to identify when logic should become a custom hook. Understanding of hook rules and why they exist.
Question 8: How Does Context API Work and When Should You Avoid It?
This question tests state management decision-making.
Weak answer: "Context replaces Redux for global state."
Strong answer: 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>;
}But context has a critical limitation: when the provider's value changes, every consumer re-renders. This makes context problematic for frequently-changing state:
// Problem: every consumer re-renders on every mouse move
const MouseContext = React.createContext({ x: 0, y: 0 });
// Moving mouse triggers re-render of entire app
function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<MouseContext.Provider value={position}>
<ExpensiveComponent /> {/* Re-renders constantly! */}
</MouseContext.Provider>
);
}When to use Context: infrequently-changing data like theme, locale, or authenticated user. When to avoid it: frequently-changing data like form state, animations, or frequently-updating lists. For those cases, consider lifting state, component composition, or a state management library with selective subscriptions.
What interviewers are looking for: Understanding of context's re-render behavior and ability to make appropriate state management decisions.
Question 9: What Is the Virtual DOM and Why Does React Use It?
This question seems basic but reveals depth of understanding.
Weak answer: "Virtual DOM is faster than the real DOM."
Strong answer: The Virtual DOM is an in-memory representation of the actual DOM elements. It's not inherently faster than direct DOM manipulation—in fact, it adds overhead. The benefit is about developer experience and batched updates.
Direct DOM manipulation is fast for single operations. But when your app state changes, you often need to update many elements. Figuring out the minimum DOM operations and batching them correctly is complex and error-prone.
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 Change → New Virtual DOM → Diff with Previous → Minimal DOM Operations
The key insight: the Virtual DOM isn't about performance in absolute terms—it's about making performance predictable while allowing declarative programming. You describe the end state, React handles the transitions.
The synchronization process between Virtual DOM and real DOM is called reconciliation. React batches multiple state updates together and applies them in one DOM operation, which is where the real performance benefit comes from.
What interviewers are looking for: Understanding that Virtual DOM is an abstraction for developer experience, not a magic performance boost. Connection to reconciliation and batching.
What These Questions Reveal
After walking through all nine questions, you'll notice they test three areas. First, understanding of React's internal mechanisms (reconciliation, Fiber, Virtual DOM). Second, ability to optimize performance systematically (memoization, re-renders, context). Third, practical patterns for production code (error boundaries, custom hooks, state management decisions).
The candidates who impress me most aren't the ones who memorize answers—they're the ones who explain trade-offs. "useMemo adds overhead, so use it when the computation is expensive." "Error Boundaries don't catch async errors, so you need try/catch there." That's the thinking senior developers bring to code reviews and architecture decisions.
Wrapping Up
These nine questions represent the React knowledge that separates senior developers from those who've just followed tutorials. They're not about memorizing APIs—they're about understanding how React works and making informed decisions about patterns and performance.
If you want to practice more questions like these, our collection includes 800+ interview questions covering React, JavaScript, TypeScript, Angular, and more—each with detailed explanations that help you understand the why, not just the what.
Related Articles
If you found this helpful, check out these related guides:
- 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
