Реконсиляція в React
Що таке Реконциляція?
Реконциляція — це алгоритм, який React використовує для порівняння двох дерев елементів та визначення, які частини реального DOM потрібно оновити.
Коли стан або пропси компонент змінюються, React створює нове дерево елементів і порівнює його з попереднім, щоб внести мінімальні зміни в DOM.
Чому це потрібно?
Оновлення DOM — це витратна операція. React оптимізує цей процес:
- Створює віртуальну копію DOM (Virtual DOM)
- Створює нове дерево, коли відбуваються зміни
- Порівнює старе та нове дерева (diffing)
- Оновлює лише змінені частини реального DOM
Як працює алгоритм
Основні правила
React використовує евристичний алгоритм O(n) замість O(n³). Він базується на двох припущеннях:
- Елементи різних типів створюють різні дерева
- Розробник може підказати, які елементи стабільні, використовуючи ключі
Порівняння елементів різних типів
Коли кореневі елементи мають різні типи, 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.
Порівняння компонентів одного типу
Коли компонент оновлюється, екземпляр залишається тим самим, тому стан зберігається:
<Counter count={1} />
// Після оновлення
<Counter count={2} />React:
- Оновлює пропси компонент
- Викликає
componentWillReceiveProps()таcomponentWillUpdate() - Викликає
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>
Ефективно!
Додавання елемента на початок (без ключа)
// Старий
<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 не зрозумів, що елементи просто зрушилися.
Ключі
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:
- Бачить новий ключ "zero" — створює елемент
- Бачить ключ "first" — елемент існував, зберігає його
- Бачить ключ "second" — елемент існував, зберігає його
Ефективно!
Правила використання ключів
Унікальність серед братів
Ключі повинні бути унікальними лише серед братніх елементів:
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>
);
}Ті ж ключі в різних масивах — це нормально.
Стабільність ключів
Ключі повинні бути стабільними та передбачуваними. Не використовуйте випадкові значення:
// Погано
{items.map(item => <Item key={Math.random()} data={item} />)}
// Погано (стан буде втрачено при перестановці)
{items.map((item, index) => <Item key={index} data={item} />)}
// Добре
{items.map(item => <Item key={item.id} data={item} />)}Чому індекс — погана ідея?
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Купити молоко', done: false },
{ id: 2, text: 'Прибрати кімнату', 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
Ви можете оптимізувати реконциляцію, повідомляючи 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
Автоматично виконує поверхневе порівняння пропсів та стану:
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, значення введення змішаються при зміні фільтра, оскільки React не зрозуміє, що це різні користувачі.
Перемикання між компонентми
function App() {
const [tab, setTab] = useState('profile');
return (
<div>
{tab === 'profile' && <Profile />}
{tab === 'settings' && <Settings />}
</div>
);
}При перемиканні вкладок React повністю демонтує один компонент і монтує інший, оскільки це різні типи.
Щоб зберегти стан, використовуйте 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>Завантаження...</div>;
}
return <span>Дані</span>;
}Коли isLoading змінюється, React демонтує весь компонент, оскільки div ≠ span.
Рішення: використовуйте той самий тип:
function Component({ isLoading }) {
return <div>{isLoading ? 'Завантаження...' : 'Дані'}</div>;
}Відсутні ключі в списках
// Погано
function List({ items }) {
return (
<ul>
{items.map(item => <li>{item.name}</li>)}
</ul>
);
}React за замовчуванням використовуватиме порядкові номери, що призведе до проблем, коли порядок змінюється.
Нестабільні ключі
// Погано
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={`${item.name}-${Date.now()}`}>
{item.name}
</li>
))}
</ul>
);
}Ключі змінюються при кожному рендері, React буде перетворювати елементи.
Висновок
Реконциляція:
- Алгоритм для порівняння дерев елементів
- Працює в O(n) завдяки евристикам
- Різні типи елементів = повна перебудова
- Одні й ті ж типи = оновлення атрибутів
- Ключі допомагають ідентифікувати елементи в списках
- Можна оптимізувати за допомогою shouldComponentUpdate, PureComponent, memo
На співбесідах:
Важливо вміти:
- Пояснити, що таке реконциляція і чому вона потрібна
- Описати, як React порівнює елементи різних і одного типу
- Пояснити важливість ключів у списках
- Навести приклади, коли індекс як ключ є поганим
- Описати методи оптимізації (PureComponent, memo)
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.