frontend / react / reconciliation.md

React Reconciliation

4 interview angles 7 min read source

React Reconciliation

Reconciliation is React’s algorithm for determining what parts of the UI have changed and need to be updated when state or props change.

Table of Contents


What is Reconciliation?

Reconciliation is the process by which React updates the DOM to match the current component state. It’s React’s “diffing” algorithm that determines what has changed and what needs to be updated.

Key Concepts:

  • Diffing: Comparing two trees to find differences
  • Batching: Grouping multiple updates together
  • Optimization: Minimizing DOM operations
  • Deterministic: Same input produces same output

Reconciliation Flow:

// 1. Component renders
function App() {
  const [count, setCount] = useState(0);
  return <div>Count: {count}</div>;
}

// 2. React creates Virtual DOM tree
const virtualDOM = {
  type: 'div',
  props: { children: 'Count: 0' }
};

// 3. Compare with previous tree
// 4. Calculate minimal changes
// 5. Apply changes to real DOM

Reconciliation Process

1. Render Phase:

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

// React creates Virtual DOM tree
const virtualTree = {
  type: 'ul',
  props: {
    children: [
      { type: 'li', props: { children: 'Learn React' } },
      { type: 'li', props: { children: 'Build app' } }
    ]
  }
};

2. Diffing Phase:

// Previous Virtual DOM
const prevTree = {
  type: 'ul',
  props: {
    children: [
      { type: 'li', props: { children: 'Learn React' } },
      { type: 'li', props: { children: 'Build app' } }
    ]
  }
};

// New Virtual DOM (after adding a todo)
const newTree = {
  type: 'ul',
  props: {
    children: [
      { type: 'li', props: { children: 'Learn React' } },
      { type: 'li', props: { children: 'Build app' } },
      { type: 'li', props: { children: 'Deploy' } } // New item
    ]
  }
};

// React identifies: Add one new li element

3. Commit Phase:

// React applies only the necessary changes
// Instead of re-rendering the entire list:
// - Keep existing li elements
// - Add only the new li element
// - No unnecessary DOM operations

Diffing Algorithm

Element Type Comparison:

// React compares elements by type
function reconcileElement(prevElement, nextElement) {
  // Different types = replace entire subtree
  if (prevElement.type !== nextElement.type) {
    return {
      type: 'REPLACE',
      element: nextElement
    };
  }

  // Same type = update in place
  return {
    type: 'UPDATE',
    element: nextElement,
    propsChanged: !shallowEqual(prevElement.props, nextElement.props)
  };
}

Props Comparison:

// Shallow comparison of props
function shallowEqual(obj1, obj2) {
  if (obj1 === obj2) return true;

  if (typeof obj1 !== 'object' || obj1 === null ||
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }

  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) return false;

  for (let key of keys1) {
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }

  return true;
}

Children Reconciliation:

// React reconciles children using keys
function reconcileChildren(prevChildren, nextChildren) {
  const updates = [];

  // Use keys to identify which children changed
  const prevKeys = new Map();
  const nextKeys = new Map();

  prevChildren.forEach((child, index) => {
    prevKeys.set(child.key || index, child);
  });

  nextChildren.forEach((child, index) => {
    nextKeys.set(child.key || index, child);
  });

  // Find additions, removals, and updates
  return updates;
}

Key Reconciliation Rules

1. Elements of Different Types:

// Different types = complete replacement
function App() {
  const [showDiv, setShowDiv] = useState(true);

  return (
    <div>
      {showDiv ? (
        <div>This is a div</div>
      ) : (
        <span>This is a span</span>
      )}
    </div>
  );
}

// When showDiv changes, React replaces the entire element
// instead of trying to update the existing one

2. Elements of Same Type:

// Same type = update in place
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="counter">
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

// React updates only the text content of h1
// The div and button remain unchanged

3. Keys in Lists:

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li> // Key helps React track items
      ))}
    </ul>
  );
}

// Without keys, React might re-render all items
// With keys, React can efficiently update only changed items

4. Component Reconciliation:

// React treats components as elements
function Parent() {
  const [user, setUser] = useState({ id: 1, name: 'John' });

  return (
    <div>
      <UserProfile user={user} />
      <UserSettings user={user} />
    </div>
  );
}

// When user changes, React re-renders both components
// but can optimize based on their props

Best Practices

1. Use Keys for Lists:

// Good: Stable keys
function GoodList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}

// Bad: No keys or unstable keys
function BadList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.text}</li> // Unstable key
      ))}
    </ul>
  );
}

2. Avoid Changing Element Types:

// Bad: Changing element types frequently
function BadComponent({ isHeader }) {
  return isHeader ? <h1>Title</h1> : <p>Title</p>;
}

// Good: Consistent element types
function GoodComponent({ isHeader }) {
  const Tag = isHeader ? 'h1' : 'h2';
  return <Tag>Title</Tag>;
}

3. Optimize with React.memo:

// Good: Prevent unnecessary re-renders
const ExpensiveComponent = React.memo(({ data }) => {
  // Expensive rendering logic
  return <div>{/* Complex UI */}</div>;
});

// Usage
function Parent({ data, otherProps }) {
  return (
    <div>
      <ExpensiveComponent data={data} />
      {/* otherProps changes won't re-render ExpensiveComponent */}
    </div>
  );
}

4. Use Stable References:

// Good: Stable function references
function GoodParent() {
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  return <Child onClick={handleClick} />;
}

// Bad: New function on every render
function BadParent() {
  return <Child onClick={() => console.log('Clicked')} />;
}

5. Batch State Updates:

// Good: Batch updates
function GoodCounter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // React batches these updates
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // Only one re-render
  };

  return <button onClick={handleClick}>Count: {count}</button>;
}

Common Interview Questions

Q: What is React reconciliation?

  • The process by which React updates the DOM to match the current component state by comparing Virtual DOM trees.

Q: How does React’s diffing algorithm work?

  • It compares elements by type, props, and children, updating only what has changed.

Q: What are the key reconciliation rules?

  • Different element types are replaced, same types are updated in place, keys help track list items.

Q: Why are keys important in reconciliation?

  • Keys help React efficiently track and update list items without re-rendering the entire list.

Q: How does React optimize reconciliation?

  • Through batching updates, shallow comparison, and minimizing DOM operations.

Q: What is the difference between render and commit phases?

  • Render phase creates Virtual DOM, commit phase applies changes to the real DOM.

Q: How does React handle component reconciliation?

  • React treats components as elements and re-renders them when props change.

Q: What is the purpose of React.memo in reconciliation?

  • It prevents unnecessary re-renders by memoizing components based on prop changes.

Summary

  • Reconciliation is React’s algorithm for updating the DOM efficiently
  • Diffing algorithm compares Virtual DOM trees to find differences
  • Key rules determine how elements are updated or replaced
  • Keys are crucial for efficient list reconciliation
  • Batching optimizes multiple state updates
  • React.memo prevents unnecessary re-renders
  • Stable references help React optimize reconciliation
  • Understanding reconciliation is key to React performance optimization

Interview angle

  • “How does React decide what to update?” - it diffs the new element tree against the previous one with two heuristics: different element type means tear down and rebuild the subtree, and key identifies children across renders. That is what makes it O(n) instead of a general tree diff.
  • “When is a component’s state destroyed?” - when its type changes at that position, or its key changes, or it disappears from the tree. Rendering {cond ? <Input /> : <Input />} keeps state; changing the wrapper type around it does not.
  • “Why does a component defined inline remount every render?” - its function identity is new each time, so the element type differs and reconciliation replaces the subtree. State loss and remounting effects are the symptoms.
  • “What is a bailout?” - React skipping re-render of a subtree when the element object is referentially identical, which is why passing children through from a parent avoids re-rendering them.