Advanced0 questionsFull Guide

react rendering reconciliation interview questions

What actually happens when state changes — re-renders, virtual DOM, diffing, keys, and the two-phase render model explained precisely.

The Mental Model

React's job is to keep the UI in sync with state. Every time state changes, React needs to figure out what the screen should look like now — and what's the minimal set of DOM operations to get it there. Understanding React well means understanding exactly how it does that. When state changes, React calls your component function again. That function returns a new tree of React elements — plain JavaScript objects describing what the UI should look like. React hasn't touched the DOM yet. It's just building a description. This is the render phase. React then compares that new description against the previous one. This comparison is called reconciliation. React walks both trees simultaneously, finds the differences, and builds a list of DOM mutations to apply. Only then — in the commit phase — does React actually touch the DOM. The critical insight that trips up most developers: re-rendering a component does NOT mean its DOM nodes are updated. A component can re-render dozens of times while React touches zero DOM nodes — because the output was identical each time. Re-render = function called again. DOM update = something actually changed. This is also why "virtual DOM is faster" is a misleading statement. The virtual DOM adds a comparison step. It's only worthwhile because it minimizes expensive DOM mutations. The speed story is: batch many state changes → reconcile once → apply minimal DOM mutations. Four things trigger a re-render: state change (setState), context change (value changed in a Provider above), parent re-render (any parent renders → child renders by default), and a forced re-render (forceUpdate, rarely used). What does NOT trigger a re-render: prop changes that don't come with a parent re-render (impossible — changing props requires the parent to re-render), re-rendering a component whose props are the same (unless it's wrapped in React.memo), and directly mutating state (React never sees the change because you didn't call setState).

The Explanation

The 4 triggers for a re-render

// Trigger 1: setState / useState setter
function Counter() {
  const [count, setCount] = useState(0)
  // Clicking the button triggers a re-render of Counter
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// Trigger 2: Context value change
const ThemeContext = createContext('light')
function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>  {/* change here... */}
      <ThemedButton />                      {/* ...re-renders this */}
    </ThemeContext.Provider>
  )
}
function ThemedButton() {
  const theme = useContext(ThemeContext)  // subscribed → re-renders on value change
  return <button className={theme}>Click</button>
}
// Trigger 3: Parent re-renders (the most common source of surprise re-renders)
function Parent() {
  const [x, setX] = useState(0)
  return <><Child /><button onClick={() => setX(x + 1)}>Update Parent</button></>
}
function Child() {
  // Child re-renders every time Parent re-renders — even though it uses no props
  console.log('Child rendered')
  return <div>Static child</div>
}
// What does NOT trigger a re-render:
const obj = { value: 1 }
obj.value = 2          // Direct mutation — React never knows. Don't do this.
props.name = 'new'     // Mutating props — also invisible to React, and breaks things.

What "Virtual DOM" actually means

// When you write JSX:
const element = <h1 className="title">Hello</h1>
// Babel compiles it to:
const element = React.createElement('h1', { className: 'title' }, 'Hello')
// Which produces a plain JavaScript object (the "virtual DOM node"):
const element = {
  type: 'h1',
  props: {
    className: 'title',
    children: 'Hello'
  },
  key: null,
  ref: null,
  // ...a few other internal fields
}
// The "virtual DOM" is just: a tree of these plain JS objects.
// React keeps TWO of these trees at all times:
// 1. The current tree (what's on screen now)
// 2. The work-in-progress tree (what should be on screen after this render)
//
// After reconciliation, work-in-progress becomes current.
// This is double buffering — more on this in the Fiber topic.

The diffing algorithm — O(n) with two heuristics

Naive tree diffing is O(n³). React achieves O(n) using two deliberate heuristics:

// Heuristic 1: Different element types → tear down, rebuild from scratch
// Before:
<div><Counter /></div>
// After:
<span><Counter /></span>
// React sees div → span. It destroys the div subtree entirely (unmounts Counter)
// and builds the span subtree fresh. Counter's state is LOST.
// This means: changing a wrapper element type loses all child state.
// Same type → update props, recurse into children
// Before: <div className="old">content</div>
// After:  <div className="new">content</div>
// React keeps the DOM node, just updates className. No unmount. State preserved.
 
// Heuristic 2: Children are reconciled using keys
// Without keys — React diffs by position:
// Before: [<A/>, <B/>, <C/>]
// After:  [<X/>, <A/>, <B/>, <C/>]
// React compares A→X (different), B→A (different), C→B (different), new→C
// Result: 3 updates + 1 insert. Expensive and wrong.
 
// With keys — React matches by identity:
// Before: [<A key="a"/>, <B key="b"/>, <C key="c"/>]
// After:  [<X key="x"/>, <A key="a"/>, <B key="b"/>, <C key="c"/>]
// React sees key "x" is new (insert), "a","b","c" moved (reorder).
// Result: 1 insert + reorder. State on A, B, C is preserved.

The index-as-key bug, shown concretely

// A todo list where items can be deleted
function TodoList() {
  const [todos, setTodos] = useState(['Buy milk', 'Read book', 'Exercise'])
  // ✗ WRONG: index as key
  return (
    <ul>
      {todos.map((todo, index) => (
        // When "Buy milk" (index 0) is deleted:
        // "Read book" becomes index 0, "Exercise" becomes index 1
        // React sees key 0 still exists — it REUSES the component instance
        // Result: wrong state, wrong focus, wrong animations
        <TodoItem key={index} text={todo} />
      ))}
    </ul>
  )
}
// ✓ CORRECT: stable unique ID as key
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 'a1', text: 'Buy milk' },
    { id: 'b2', text: 'Read book' },
    { id: 'c3', text: 'Exercise' }
  ])
  return (
    <ul>
      {todos.map(todo => (
        // When "Buy milk" (id: a1) is deleted:
        // React sees key "a1" is gone → unmounts that instance
        // "b2" and "c3" still exist → reuses those instances with correct state
        <TodoItem key={todo.id} text={todo.text} />
      ))}
    </ul>
  )
}
// When IS index-as-key acceptable?
// Only when ALL three conditions are true:
// 1. The list is static — never reordered or filtered
// 2. Items are never inserted or deleted from the middle
// 3. Items have no internal state (they're purely display components)

Render phase vs Commit phase

// RENDER PHASE — pure, no side effects, may be interrupted (in Concurrent Mode)
// React calls your component functions
// Reconciles old tree vs new tree
// Builds a list of "effects" (DOM mutations needed)
// This phase can be paused, restarted, or thrown away entirely.
function MyComponent() {
  // This runs in the RENDER phase:
  const value = expensiveCalculation()  // runs on every render
  const [state] = useState(0)           // reads fiber state
  // No DOM access here. No network calls. Just return elements.
  return <div>{value}</div>
}
// COMMIT PHASE — synchronous, cannot be interrupted
// Phase 1 (Before Mutation): getSnapshotBeforeUpdate
// Phase 2 (Mutation): React applies DOM mutations
// Phase 3 (Layout): useLayoutEffect fires synchronously
// Then browser paints the screen
// Phase 4 (Passive Effects): useEffect fires asynchronously after paint
// useEffect vs useLayoutEffect — the key distinction:
function Modal() {
  const ref = useRef()
  // useLayoutEffect: fires BEFORE browser paint
  // Use when you need to read/mutate DOM before user sees it
  useLayoutEffect(() => {
    // Safe: DOM mutations here are invisible to the user
    const height = ref.current.getBoundingClientRect().height
    ref.current.style.marginTop = `-${height / 2}px`  // center vertically
  })
  // useEffect: fires AFTER browser paint
  // Use for data fetching, subscriptions, logging — most things
  useEffect(() => {
    // DOM mutations here cause a visible flicker (layout → paint → update)
    const height = ref.current.getBoundingClientRect().height
    ref.current.style.marginTop = `-${height / 2}px`  // user sees shift!
  })
  return <div ref={ref}>Modal content</div>
}
// Rule of thumb:
// Need to read/write DOM that affects layout → useLayoutEffect
// Everything else → useEffect

The "re-renders everything, updates few DOM nodes" model

// Imagine a parent with 5 children:
function Dashboard() {
  const [activeTab, setActiveTab] = useState('home')
  // When setActiveTab is called:
  // ALL 5 children re-render (their functions run)
  // React compares old output vs new output for each
  // DOM updates: maybe 1-2 className changes, nothing else
  return (
    <div>
      <Nav activeTab={activeTab} />         {/* function called — output changes */}
      <TabContent tab={activeTab} />        {/* function called — output changes */}
      <Sidebar />                           {/* function called — same output */}
      <Footer />                            {/* function called — same output */}
      <NotificationBell />                  {/* function called — same output */}
    </div>
  )
}
// Re-renders: 5 (all children ran)
// DOM mutations: 2 (Nav className, TabContent innerHTML)
// This is the trade-off React makes:
// Cheap JS function calls (re-renders) → minimal expensive DOM operations
// To prevent unnecessary re-renders on Sidebar, Footer, NotificationBell:
const Sidebar = React.memo(function Sidebar() { ... })
const Footer = React.memo(function Footer() { ... })
const NotificationBell = React.memo(function NotificationBell() { ... })
// Now only Nav and TabContent re-render when activeTab changes

Common Misconceptions

⚠️

Many developers believe "re-render = DOM update." This is the most important misconception to correct. Re-rendering means React called your component function again and ran the reconciliation algorithm. DOM updates are a separate step that only happens in the commit phase when reconciliation finds actual differences. A component can re-render hundreds of times without changing a single DOM node if the output is identical each time.

⚠️

Many developers believe "the virtual DOM is faster than the real DOM." The virtual DOM adds overhead — it requires building a JS object tree, diffing it against the previous tree, and then applying changes. It's only faster than naive DOM manipulation because naive DOM manipulation doesn't batch changes. The virtual DOM wins against "update everything on every state change." It doesn't win against "update exactly the right DOM nodes." The real benefit is the programming model, not raw speed.

⚠️

Many developers believe changing a prop causes a re-render. This is backwards — a prop change requires the parent to re-render and pass new values, which is what causes the child to re-render. The prop change is a consequence of the parent's re-render, not an independent trigger. If the parent doesn't re-render, the child's props can never change.

⚠️

Many developers believe React.memo prevents all unnecessary re-renders. React.memo only prevents re-renders caused by a parent's re-render when the props are shallowly equal. It does nothing when the component's own state changes, when a context it subscribes to changes, or when the props contain new object/array/function references on every render.

⚠️

Many developers think useEffect and useLayoutEffect are interchangeable with different timing. The critical difference is that useLayoutEffect fires synchronously after DOM mutation but before the browser paints. This means DOM reads inside useLayoutEffect see the post-update DOM. DOM writes inside it are invisible to the user. If you use useEffect for DOM measurements that affect layout, users will see a flash — the component renders with incorrect measurements, browser paints, then effect runs and corrects the measurements.

⚠️

Many developers think keys are just for avoiding the "key warning." Keys are React's identity system. A key is how React determines whether a component instance should be reused or torn down and rebuilt. Giving the same component a different key forces a full remount — useful for resetting state. This "key trick" is a legitimate pattern: to reset a child's state when a prop changes, just change its key.

Where You'll See This in Real Code

Form field state loss is a direct consequence of the type-change heuristic. If a component conditionally renders either a TextInput or a SelectInput at the same tree position, switching between them causes the internal state (value, focus, cursor position) to be destroyed completely — React sees a different element type at that position and unmounts. The fix is to always render both and toggle visibility with CSS, or give each a different key to manage the remount explicitly.

Animated list reordering bugs trace directly to index-as-key mistakes. In a sortable kanban board, if list items use index as key, dragging a card from position 0 to position 3 causes React to see "key 0 changed its text" rather than "key 'abc123' moved to a new position." All CSS transitions fire on the wrong elements, inputs lose their values, and focused elements lose focus. Libraries like Framer Motion's AnimatePresence require stable keys to correctly animate enter/exit of list items.

The "stale props in event handlers" bug in real-time applications comes from the render/commit phase distinction. A chat app that subscribes to a WebSocket in useEffect and reads the latest messages in a callback has a stale closure — the callback captures the messages array from the render when the effect ran, not the latest value. The fix is useRef to always point to the latest value, or including the dependency in the effect's array to re-subscribe when messages change.

Performance profiling reveals that most "slow" React apps are caused by excessive re-renders, not slow render functions. A dashboard at a fintech startup had a 200ms update lag because a top-level state update (user notification count) was triggering re-renders in 40+ components. The fix was colocating state closer to where it was needed and wrapping stable subtrees with React.memo — reducing re-renders from 40 to 3 on each notification, bringing update lag to under 16ms.

Server components in Next.js App Router rely directly on the render/commit model. Server components run their render phase on the server — they produce React element trees (plain JSON) that get sent to the client. The client merges this into the existing fiber tree without discarding existing client component state. This is only possible because React separates "what should the UI look like" (render) from "apply changes to the DOM" (commit).

Interview Cheat Sheet

  • 4 re-render triggers: state change, context value change, parent re-render, forceUpdate
  • Re-render ≠ DOM update — function called again ≠ DOM node changed
  • Virtual DOM = plain JS objects describing UI; React keeps current + work-in-progress trees
  • Diffing is O(n): different types → full subtree teardown; same type → props diff + recurse
  • Keys are identity: stable unique ID → reuse instance + state; index → bugs on reorder/delete
  • Key trick: change a component's key to force full remount and reset its state
  • Render phase: pure, no side effects, may be interrupted; Commit phase: synchronous, DOM mutations
  • useLayoutEffect: fires before browser paint — for DOM reads/writes that affect layout
  • useEffect: fires after browser paint — for subscriptions, data fetching, logging
  • React.memo: prevents re-render from parent if props are shallowly equal; doesn't help for own state/context
  • Colocate state: state that only one subtree needs should live in that subtree, not at the top
  • Never mutate state directly — React uses reference equality to detect changes; mutation is invisible
💡

How to Answer in an Interview

  • 1.When asked "what is reconciliation", ground your answer in the two-phase model: "Reconciliation is the render phase — React calls component functions to build a new element tree, then diffs it against the previous tree to find the minimal set of DOM mutations. The actual mutations happen in a separate commit phase. These phases being separate is what allows Concurrent Mode to interrupt rendering without corrupting the UI."
  • 2.The "what triggers a re-render" question separates candidates who understand React from those who've just used it. List all four triggers — state, context, parent re-render, forceUpdate — and importantly, mention what does NOT trigger one: directly mutating state. Then explain why parent re-renders cascade to children by default, and how React.memo intercepts that cascade.
  • 3.The index-as-key question is a concrete coding question at Swiggy, Flipkart, and similar companies. Show the broken behavior: deleting item at index 0 causes all items below to get new keys, which causes React to reuse the wrong component instances, which causes stale state and broken animations. Then explain the fix (stable IDs) and the one exception (static, non-stateful, never-reordered lists).
  • 4.For "what is the virtual DOM", avoid the "it's faster" answer — interviewers know that's incomplete. Say: "The virtual DOM is a JavaScript object tree describing the UI. React keeps two of these trees — the current (on-screen) state and a work-in-progress (new) state. After diffing them, React derives the minimal DOM mutations needed. The value isn't raw speed — it's that you get declarative code and React handles the imperative DOM operations."
  • 5.When asked about useEffect vs useLayoutEffect, lead with the visual consequence: "If you need to measure a DOM element and adjust its position before the user sees it — like centering a modal — use useLayoutEffect. It fires after DOM mutation but before the browser paints, so the user never sees the intermediate state. useEffect fires after paint, which means visual adjustments there cause a flash. For everything else — subscriptions, data fetching, logging — useEffect is correct."

Practice Questions

No questions tagged to this topic yet.

Related Topics

React Fiber Architecture Interview Questions
Advanced·4–8 Qs
Concurrent Rendering (React 18) Interview Questions
Advanced·4–8 Qs
React Rendering & Performance Interview Questions
Advanced·4–8 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz