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:
- Creates a virtual copy of the DOM (Virtual DOM)
- Creates a new tree when changes occur
- Compares old and new trees (diffing)
- 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:
- Elements of different types produce different trees
- 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:
- Removes
<div>and all children - Unmounts
Counter(callscomponentWillUnmount) - Creates new
<span> - Mounts new
Counter(callscomponentDidMount)
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:
- Updates component props
- Calls
componentWillReceiveProps()andcomponentWillUpdate() - Calls
render() - 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:
- Compares first
<li>— identical, does nothing - Compares second
<li>— identical, does nothing - 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:
- Compares first
<li>— "first" ≠ "zero", updates - Compares second
<li>— "second" ≠ "first", updates - 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:
- Sees new key "zero" — creates element
- Sees key "first" — element existed, keeps it
- 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 div ≠ span.
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)