Hack Frontend Community

Reconciliation in React

What is Reconciliation?

Reconciliation is the algorithm React uses to compare two element trees and determine which parts of the real DOM need to be updated.

When a component's state or props change, React creates a new element tree and compares it with the previous one to make minimal changes to the DOM.


Why is it needed?

Updating the DOM is an expensive operation. React optimizes this process:

  1. Creates a virtual copy of the DOM (Virtual DOM)
  2. Creates a new tree when changes occur
  3. Compares old and new trees (diffing)
  4. Updates only changed parts of the real DOM

How the algorithm works

Basic Rules

React uses a heuristic O(n) algorithm instead of O(n³). It's based on two assumptions:

  1. Elements of different types produce different trees
  2. The developer can hint which elements are stable using keys

Comparing elements of different types

When root elements have different types, React removes the old tree and builds a new one from scratch.

// Old tree
<div>
  <Counter />
</div>

// New tree
<span>
  <Counter />
</span>

React:

  1. Removes <div> and all children
  2. Unmounts Counter (calls componentWillUnmount)
  3. Creates new <span>
  4. Mounts new Counter (calls componentDidMount)

Comparing elements of the same type

DOM Elements

When types match, React compares attributes and updates only changed ones:

// Old
<div className="before" title="old" />

// New
<div className="after" title="old" />

React will update only className, title stays unchanged.

Styles

// Old
<div style={{color: 'red', fontWeight: 'bold'}} />

// New
<div style={{color: 'blue', fontWeight: 'bold'}} />

React updates only color.


Comparing components of the same type

When a component updates, the instance remains the same, so state is preserved:

<Counter count={1} />

// After update
<Counter count={2} />

React:

  1. Updates component props
  2. Calls componentWillReceiveProps() and componentWillUpdate()
  3. Calls render()
  4. Runs reconciliation on render result

Recursive children processing

React recursively processes children. Let's look at an example:

Adding element to the end

// Old
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// New
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React:

  1. Compares first <li> — identical, does nothing
  2. Compares second <li> — identical, does nothing
  3. Adds third <li>

Efficient!

Adding element to the beginning (without key)

// Old
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// New
<ul>
  <li>zero</li>
  <li>first</li>
  <li>second</li>
</ul>

React:

  1. Compares first <li> — "first" ≠ "zero", updates
  2. Compares second <li> — "second" ≠ "first", updates
  3. Adds third <li>

Inefficient! React didn't understand that elements just shifted.


Keys

key helps React understand which elements changed, were added, or removed.

With keys

// Old
<ul>
  <li key="first">first</li>
  <li key="second">second</li>
</ul>

// New
<ul>
  <li key="zero">zero</li>
  <li key="first">first</li>
  <li key="second">second</li>
</ul>

React:

  1. Sees new key "zero" — creates element
  2. Sees key "first" — element existed, keeps it
  3. Sees key "second" — element existed, keeps it

Efficient!


Key usage rules

Uniqueness among siblings

Keys need to be unique only among sibling elements:

function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map(post =>
        <li key={post.id}>{post.title}</li>
      )}
    </ul>
  );
  
  const content = props.posts.map(post =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

Same keys in different arrays is fine.

Key stability

Keys should be stable and predictable. Don't use random values:

// Bad
{items.map(item => <Item key={Math.random()} data={item} />)}

// Bad (state will be lost on reordering)
{items.map((item, index) => <Item key={index} data={item} />)}

// Good
{items.map(item => <Item key={item.id} data={item} />)}

Why index is a bad idea?

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Clean room', done: true }
  ]);

  // Bad
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} />
      ))}
    </ul>
  );
}

Problem: when removing the first element, indices shift, and React thinks the content changed, not the order.

// Good
return (
  <ul>
    {todos.map(todo => (
      <TodoItem key={todo.id} todo={todo} />
    ))}
  </ul>
);

Optimizations

shouldComponentUpdate

You can optimize reconciliation by telling React when a component DOESN'T need re-rendering:

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Render only if id changed
    return this.props.id !== nextProps.id;
  }

  render() {
    return <div>{this.props.data}</div>;
  }
}

React.PureComponent

Automatically performs shallow comparison of props and state:

class MyComponent extends React.PureComponent {
  render() {
    return <div>{this.props.data}</div>;
  }
}

React.memo

For functional components:

const MyComponent = React.memo(function MyComponent(props) {
  return <div>{props.data}</div>;
});

// With custom comparison
const MyComponent = React.memo(
  function MyComponent(props) {
    return <div>{props.data}</div>;
  },
  (prevProps, nextProps) => {
    return prevProps.id === nextProps.id;
  }
);

Practical examples

List with filtering

function UserList({ users, filter }) {
  const filteredUsers = users.filter(user =>
    user.name.includes(filter)
  );

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>
          {user.name}
          <input type="text" />
        </li>
      ))}
    </ul>
  );
}

If using index instead of user.id, input values will get mixed up when changing the filter because React won't understand these are different users.

Switching between components

function App() {
  const [tab, setTab] = useState('profile');

  return (
    <div>
      {tab === 'profile' && <Profile />}
      {tab === 'settings' && <Settings />}
    </div>
  );
}

When switching tabs, React fully unmounts one component and mounts another because they're different types.

To preserve state, use display: none:

function App() {
  const [tab, setTab] = useState('profile');

  return (
    <div>
      <div style={{ display: tab === 'profile' ? 'block' : 'none' }}>
        <Profile />
      </div>
      <div style={{ display: tab === 'settings' ? 'block' : 'none' }}>
        <Settings />
      </div>
    </div>
  );
}

Common mistakes

Changing element type

function Component({ isLoading }) {
  if (isLoading) {
    return <div>Loading...</div>;
  }
  return <span>Data</span>;
}

When isLoading changes, React unmounts the entire component because divspan.

Solution: use the same type:

function Component({ isLoading }) {
  return <div>{isLoading ? 'Loading...' : 'Data'}</div>;
}

Missing keys in lists

// Bad
function List({ items }) {
  return (
    <ul>
      {items.map(item => <li>{item.name}</li>)}
    </ul>
  );
}

React will use ordinal numbers by default, leading to problems when order changes.

Unstable keys

// Bad
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={`${item.name}-${Date.now()}`}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Key changes on every render, React will recreate elements.


Conclusion

Reconciliation:

  • Algorithm for comparing element trees
  • Works in O(n) thanks to heuristics
  • Different element types = complete rebuild
  • Same types = attribute updates
  • Keys help identify elements in lists
  • Can be optimized with shouldComponentUpdate, PureComponent, memo

In interviews:

Important to be able to:

  • Explain what reconciliation is and why it's needed
  • Describe how React compares elements of different and same types
  • Explain the importance of keys in lists
  • Give examples of when index as key is bad
  • Describe optimization methods (PureComponent, memo)