Reconciliation (Согласование) в React
Что такое Reconciliation?
Reconciliation (Согласование) — это алгоритм, который React использует для сравнения двух деревьев элементов и определения, какие части реального DOM нужно обновить.
Когда state или props компонента меняются, React создаёт новое дерево элементов и сравнивает его с предыдущим, чтобы сделать минимальное количество изменений в DOM.
Зачем это нужно?
Обновление DOM — дорогая операция. React оптимизирует этот процесс:
- Создаёт виртуальную копию DOM (Virtual DOM)
- При изменениях создаёт новое дерево
- Сравнивает старое и новое дерево (diffing)
- Обновляет только изменённые части реального DOM
Как работает алгоритм
Основные правила
React использует эвристический алгоритм O(n) вместо O(n³). Он основан на двух предположениях:
- Элементы разных типов создают разные деревья
- Разработчик может указать, какие элементы стабильны с помощью key
Сравнение элементов разных типов
Когда корневые элементы имеют разные типы, React удаляет старое дерево и строит новое с нуля.
// Старое дерево
<div>
<Counter />
</div>
// Новое дерево
<span>
<Counter />
</span>
React:
- Удаляет
<div>и всех детей - Размонтирует
Counter(вызоветcomponentWillUnmount) - Создаёт новый
<span> - Монтирует новый
Counter(вызоветcomponentDidMount)
Сравнение элементов одного типа
DOM-элементы
Когда типы совпадают, React сравнивает атрибуты и обновляет только изменённые:
// Старый
<div className="before" title="old" />
// Новый
<div className="after" title="old" />
React обновит только className, title останется без изменений.
Стили
// Старый
<div style={{color: 'red', fontWeight: 'bold'}} />
// Новый
<div style={{color: 'blue', fontWeight: 'bold'}} />
React обновит только color.
Сравнение компонентов одного типа
Когда компонент обновляется, экземпляр остаётся тем же, поэтому state сохраняется:
<Counter count={1} />
// После обновления
<Counter count={2} />
React:
- Обновляет props компонента
- Вызывает
componentWillReceiveProps()иcomponentWillUpdate() - Вызывает
render() - Запускает reconciliation для результата
render()
Рекурсивная обработка детей
React рекурсивно обрабатывает детей. Рассмотрим пример:
Добавление элемента в конец
// Старый
<ul>
<li>first</li>
<li>second</li>
</ul>
// Новый
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React:
- Сравнивает первый
<li>— одинаковые, ничего не делает - Сравнивает второй
<li>— одинаковые, ничего не делает - Добавляет третий
<li>
Эффективно!
Добавление элемента в начало (без key)
// Старый
<ul>
<li>first</li>
<li>second</li>
</ul>
// Новый
<ul>
<li>zero</li>
<li>first</li>
<li>second</li>
</ul>
React:
- Сравнивает первый
<li>— "first" ≠ "zero", обновляет - Сравнивает второй
<li>— "second" ≠ "first", обновляет - Добавляет третий
<li>
Неэффективно! React не понял, что элементы просто сдвинулись.
Ключи (Keys)
key помогает React понять, какие элементы изменились, добавились или удалились.
С ключами
// Старый
<ul>
<li key="first">first</li>
<li key="second">second</li>
</ul>
// Новый
<ul>
<li key="zero">zero</li>
<li key="first">first</li>
<li key="second">second</li>
</ul>
React:
- Видит новый key "zero" — создаёт элемент
- Видит key "first" — элемент уже был, оставляет
- Видит key "second" — элемент уже был, оставляет
Эффективно!
Правила использования keys
Уникальность среди соседей
Ключи должны быть уникальными только среди соседних элементов:
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>
);
}
Одинаковые key в разных массивах — это нормально.
Стабильность ключей
Ключи должны быть стабильными и предсказуемыми. Не используйте случайные значения:
// Плохо
{items.map(item => <Item key={Math.random()} data={item} />)}
// Плохо (при пересортировке потеряется state)
{items.map((item, index) => <Item key={index} data={item} />)}
// Хорошо
{items.map(item => <Item key={item.id} data={item} />)}
Почему index — плохая идея?
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Clean room', done: true }
]);
// Плохо
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
</ul>
);
}
Проблема: при удалении первого элемента, индексы смещаются, и React думает, что изменился контент, а не порядок.
// Хорошо
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
Оптимизации
shouldComponentUpdate
Можно оптимизировать reconciliation, сказав React, когда компонент НЕ нужно перерисовывать:
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Рендерить только если изменился id
return this.props.id !== nextProps.id;
}
render() {
return <div>{this.props.data}</div>;
}
}
React.PureComponent
Автоматически делает поверхностное сравнение props и state:
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.data}</div>;
}
}
React.memo
Для функциональных компонентов:
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.data}</div>;
});
// С кастомным сравнением
const MyComponent = React.memo(
function MyComponent(props) {
return <div>{props.data}</div>;
},
(prevProps, nextProps) => {
return prevProps.id === nextProps.id;
}
);
Примеры из практики
Список с фильтрацией
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>
);
}
Если использовать index вместо user.id, то при изменении фильтра значения в <input> перемешаются, потому что React не поймёт, что это другие пользователи.
Переключение между компонентами
function App() {
const [tab, setTab] = useState('profile');
return (
<div>
{tab === 'profile' && <Profile />}
{tab === 'settings' && <Settings />}
</div>
);
}
При переключении tab React полностью размонтирует один компонент и монтирует другой, потому что это разные типы.
Если хотите сохранить state, используйте 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>
);
}
Частые ошибки
Изменение типа элемента
function Component({ isLoading }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <span>Data</span>;
}
При изменении isLoading React размонтирует весь компонент, потому что div ≠ span.
Решение: используйте одинаковый тип:
function Component({ isLoading }) {
return <div>{isLoading ? 'Loading...' : 'Data'}</div>;
}
Отсутствие keys в списках
// Плохо
function List({ items }) {
return (
<ul>
{items.map(item => <li>{item.name}</li>)}
</ul>
);
}
React будет использовать порядковый номер по умолчанию, что приведёт к проблемам при изменении порядка.
Нестабильные keys
// Плохо
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={`${item.name}-${Date.now()}`}>
{item.name}
</li>
))}
</ul>
);
}
Key меняется при каждом рендере, React будет пересоздавать элементы.
Вывод
Reconciliation:
- Алгоритм сравнения деревьев элементов
- Работает за O(n) благодаря эвристикам
- Разные типы элементов = полная пересборка
- Одинаковые типы = обновление атрибутов
- Keys помогают идентифицировать элементы в списках
- Можно оптимизировать с помощью shouldComponentUpdate, PureComponent, memo
На собеседовании:
Важно уметь:
- Объяснить, что такое reconciliation и зачем он нужен
- Рассказать, как React сравнивает элементы разных и одинаковых типов
- Объяснить важность keys в списках
- Привести примеры, когда index как key — плохая идея
- Рассказать о способах оптимизации (PureComponent, memo)