9+ Advanced React Interview Questions 2025: Fiber, Reconciliation & Performance

·12 min read
reactinterview-questionsadvanced-reactreact-fiberperformancefrontend

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. These advanced questions test deep understanding of React internals and performance patterns that senior developers are expected to know.

Table of Contents

  1. React Internals Questions
  2. React Hooks Questions
  3. Performance Optimization Questions
  4. Error Handling Questions
  5. State Management Questions
  6. Quick Reference

React Internals Questions

These questions test your understanding of how React works under the hood.

How does React reconciliation work?

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.

Using array index as a key is problematic because 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.

What is React Fiber and why was it introduced?

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 is the Virtual DOM and why does React use it?

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.


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: 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."

When should you use useLayoutEffect instead of useEffect?

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.

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(() => {
        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

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?

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.


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, 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 works well in production 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.


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>;
}

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.


Quick Reference

TopicKey Points
ReconciliationO(n) diffing using type heuristic and keys; explains why keys matter
FiberIncremental rendering, priority scheduling, foundation for concurrent features
Virtual DOMAbstraction for declarative UI; enables batched updates
useMemoMemoizes computed values; use for expensive calculations
useCallbackMemoizes function references; use for callbacks to memoized children
useLayoutEffectRuns before paint; use for DOM measurements to prevent flicker
Custom HooksExtract stateful logic; must start with use, follow hook rules
Re-rendersCaused by parent re-renders, new references, context changes
Error BoundariesClass components with getDerivedStateFromError; don't catch async/event errors
ContextAvoid for frequently-changing data; split contexts to minimize re-renders

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides