# React reconciliation та keys простими словами: чому index як key ламає список

> Як React порівнює дерева при ре-рендері, навіщо потрібні keys і чому key={index} ламає списки з інпутами. Повний приклад до/після, правила вибору key і питання зі співбесід.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-09-24
- Категорія: React і Frontend
- Canonical: https://cookiesoftware.io/blog/react-reconciliation-keys

---

**Коротка відповідь.** 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»](https://react.dev/learn/preserving-and-resetting-state) – якщо читати один першоджерельний матеріал про reconciliation, то саме його.

## Повний приклад поломки: key={index} + інпути

Список гостей, у кожного – неконтрольований інпут для нотатки. Видаляємо першого гостя:

```tsx
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). Стан інпутів прив'язався до позицій, а не до людей.

Фікс – один рядок:

```tsx
{guests.map((guest) => (
  <li key={guest.id}>
    …
  </li>
))}
```

Тепер при видаленні Олі зникає вузол із key='a', а нотатки Тараса й Ніни лишаються біля своїх власників. Той самий ефект стосується контрольованих інпутів зі станом у дочірньому компоненті, фокуса, скролу, CSS-анімацій і будь-якого `useState` всередині елемента списку.

## Коли index як key допустимий

Чесна відповідь «залежить» тут має чіткі межі. Індекс безпечний, коли виконуються **всі три** умови:

1. список ніколи не переупорядковується і не фільтрується;
2. елементи не додаються/не видаляються з середини;
3. у рядках немає стану (інпутів, розкривних панелей, анімацій).

Типовий приклад – статичний список переваг на лендингу. Але навіть там `key={item.id}` не коштує нічого, тож правило за замовчуванням просте: **стабільний ідентифікатор із даних**. Якщо його немає – згенеруй при створенні запису (наприклад, `crypto.randomUUID()` у момент додавання), а не при рендері: ключ, створений у `map`, змінюється щорендеру і змушує React перестворювати все.

## Ще два практичні наслідки reconciliation

**Скидання стану через key.** Ключі працюють не лише у списках. Змінюючи `key`, ти явно кажеш React'у «це інший елемент – знищ стан і створи заново»:

```tsx
<ProfileForm key={userId} userId={userId} />
```

Перемкнули користувача – форма гарантовано чиста, без ручного «очищення» полів в ефекті. Це ідіоматичний патерн, а не хак.

**Компонент, оголошений усередині компонента.** Кожен рендер створює нову функцію, для React це щоразу «інший тип» – піддерево перестворюється, стан губиться:

```tsx
function Parent() {
  // ❌ новий тип на кожен рендер
  function Child() { … }
  return <Child />;
}
```

Оголошуй компоненти на верхньому рівні модуля. Це часте питання-пастка після базових питань про keys.

## Питання зі співбесід по цій темі

- **«Що таке reconciliation?»** – процес порівняння нового дерева елементів зі старим, щоб мінімально оновити DOM.
- **«Навіщо key, якщо все й так працює?»** – без key React порівнює дітей за позицією; key дає ідентичність елемента між рендерами.
- **«Чому не можна key={Math.random()}?»** – ключ змінюється щорендеру → React вважає всі елементи новими → повне перестворення DOM і втрата стану.
- **«Чи впливають keys на продуктивність?»** – так: стабільні ключі дозволяють переставляти вузли замість перестворення. Але головний ефект – коректність стану, продуктивність вторинна. Ширше про перформанс – у статті про [оптимізацію React](/blog/optymizatsiia-react-performance).
- **«Як скинути стан дочірнього компонента?»** – змінити його key (приклад із формою вище).

Ці питання – частина типового блоку про рендеринг; повна карта підготовки є в гайді [React співбесіда: як підготуватися](/blog/react-interview-guide), а потренувати механіку руками найшвидше на [задачах live coding із TypeScript](/blog/react-live-coding-typescript).

## Практичне завдання

Відтвори зламаний приклад із гостями в пісочниці. Спочатку переконайся, що бачиш баг із key={index}. Потім: 1) полагодь через `guest.id`; 2) додай кнопку «перемішати список» і подивись, як поводяться нотатки в обох варіантах; 3) поясни вголос (собі або колезі), чому відбувається саме так. Пояснення вголос – найкращий тест, чи ти справді розумієш reconciliation, а не запам'ятав правило.
