Коротка відповідь. Reconciliation – це процес, яким React вирішує, що саме змінити в DOM після ре-рендеру: він порівнює нове дерево елементів зі старим. Для списків це порівняння спирається на key: ключ каже React'у «це той самий елемент, що й минулого разу». Якщо ключем зробити індекс масиву, то при вставці чи видаленні елементів ключі «з'їжджають» – і React прив'язує стан (інпути, фокус, анімації) не до тих елементів. Розберімо механіку і повний приклад поломки.
Як React вирішує, що оновити
Після кожного оновлення стану React викликає компонент і отримує нове дерево елементів – легких об'єктів-описів. Далі порівнює його з попереднім за простими правилами:
- Той самий тип елемента (
div→div,TodoItem→TodoItem) – React зберігає DOM-вузол і стан компонента, оновлює лише пропси, що змінилися. - Інший тип (
div→span,TodoItem→Note) – старе піддерево знищується разом зі станом, нове створюється з нуля. - Списки дітей – порівнюються за
key: елементи з однаковими ключами вважаються «тими самими», навіть якщо переїхали на іншу позицію.
Головний наслідок, який перевіряють на співбесідах: стан живе за позицією в дереві та ключем, а не «всередині» твоїх даних. Це детально пояснено в офіційному розділі react.dev «Preserving and Resetting State» – якщо читати один першоджерельний матеріал про reconciliation, то саме його.
Повний приклад поломки: key={index} + інпути
Список гостей, у кожного – неконтрольований інпут для нотатки. Видаляємо першого гостя:
import { useState } from 'react';
type Guest = { id: string; name: string };
const initial: Guest[] = [
{ id: 'a', name: 'Оля' },
{ id: 'b', name: 'Тарас' },
{ id: 'c', name: 'Ніна' },
];
export function GuestListBroken() {
const [guests, setGuests] = useState(initial);
return (
<ul>
{guests.map((guest, index) => (
<li key={index}>
{guest.name}
<input placeholder="нотатка" />
<button onClick={() => setGuests((prev) => prev.filter((g) => g.id !== guest.id))}>
видалити
</button>
</li>
))}
</ul>
);
}Сценарій поломки: введи «алергія на горіхи» в нотатку Олі (перший рядок) і видали Олю. Текст «алергія на горіхи» залишиться в першому рядку – тепер біля Тараса.
Чому: до видалення ключі були 0:Оля, 1:Тарас, 2:Ніна. Після видалення масив став [Тарас, Ніна], а ключі – знову 0 і 1. React бачить: «елемент із key=0 був і є» – і зберігає його DOM разом зі значенням інпута, просто оновивши текст імені. Зник не «рядок Олі», а останній рядок (key=2). Стан інпутів прив'язався до позицій, а не до людей.
Фікс – один рядок:
{guests.map((guest) => (
<li key={guest.id}>
…
</li>
))}Тепер при видаленні Олі зникає вузол із key='a', а нотатки Тараса й Ніни лишаються біля своїх власників. Той самий ефект стосується контрольованих інпутів зі станом у дочірньому компоненті, фокуса, скролу, CSS-анімацій і будь-якого useState всередині елемента списку.
Коли index як key допустимий
Чесна відповідь «залежить» тут має чіткі межі. Індекс безпечний, коли виконуються всі три умови:
- список ніколи не переупорядковується і не фільтрується;
- елементи не додаються/не видаляються з середини;
- у рядках немає стану (інпутів, розкривних панелей, анімацій).
Типовий приклад – статичний список переваг на лендингу. Але навіть там key={item.id} не коштує нічого, тож правило за замовчуванням просте: стабільний ідентифікатор із даних. Якщо його немає – згенеруй при створенні запису (наприклад, crypto.randomUUID() у момент додавання), а не при рендері: ключ, створений у map, змінюється щорендеру і змушує React перестворювати все.
Ще два практичні наслідки reconciliation
Скидання стану через key. Ключі працюють не лише у списках. Змінюючи key, ти явно кажеш React'у «це інший елемент – знищ стан і створи заново»:
<ProfileForm key={userId} userId={userId} />Перемкнули користувача – форма гарантовано чиста, без ручного «очищення» полів в ефекті. Це ідіоматичний патерн, а не хак.
Компонент, оголошений усередині компонента. Кожен рендер створює нову функцію, для React це щоразу «інший тип» – піддерево перестворюється, стан губиться:
function Parent() {
// ❌ новий тип на кожен рендер
function Child() { … }
return <Child />;
}Оголошуй компоненти на верхньому рівні модуля. Це часте питання-пастка після базових питань про keys.
Питання зі співбесід по цій темі
- «Що таке reconciliation?» – процес порівняння нового дерева елементів зі старим, щоб мінімально оновити DOM.
- «Навіщо key, якщо все й так працює?» – без key React порівнює дітей за позицією; key дає ідентичність елемента між рендерами.
- «Чому не можна key={Math.random()}?» – ключ змінюється щорендеру → React вважає всі елементи новими → повне перестворення DOM і втрата стану.
- «Чи впливають keys на продуктивність?» – так: стабільні ключі дозволяють переставляти вузли замість перестворення. Але головний ефект – коректність стану, продуктивність вторинна. Ширше про перформанс – у статті про оптимізацію React.
- «Як скинути стан дочірнього компонента?» – змінити його key (приклад із формою вище).
Ці питання – частина типового блоку про рендеринг; повна карта підготовки є в гайді React співбесіда: як підготуватися, а потренувати механіку руками найшвидше на задачах live coding із TypeScript.
Практичне завдання
Відтвори зламаний приклад із гостями в пісочниці. Спочатку переконайся, що бачиш баг із key={index}. Потім: 1) полагодь через guest.id; 2) додай кнопку «перемішати список» і подивись, як поводяться нотатки в обох варіантах; 3) поясни вголос (собі або колезі), чому відбувається саме так. Пояснення вголос – найкращий тест, чи ти справді розумієш reconciliation, а не запам'ятав правило.