Чому в React потрібен ключ?
key є спеціальним пропом React, який унікально ідентифікує елементи списку, щоб алгоритм reconciliation (узгодження) міг відстежувати додавання, видалення та перестановки без повного перебудування DOM.
Теорія
TL;DR
- Key схожий на бейдж на конференції: React використовує його, щоб зіставити "старий елемент" з "новим" між рендерами, а не перевіряти всіх за позицією в черзі.
- Без key React порівнює по індексу. Будь-яка перестановка змушує оновити кожен елемент після зміни.
- Зі стабільними key React зіставляє по ідентичності та оновлює лише те, що реально змінилось.
- Правило вибору: статичний список без перестановок, тоді index підійде. Динамічний список із перестановками або локальним станом, тоді потрібен стабільний унікальний ID.
Короткий приклад
// Без key: React попереджає і порівнює по позиції
const BadList = ({ fruits }) => (
<ul>
{fruits.map(fruit => <li>{fruit.name}</li>)}
</ul>
);
// Зі стабільними key: React відстежує кожен елемент за ідентичністю
const GoodList = ({ fruits }) => (
<ul>
{fruits.map(fruit => <li key={fruit.id}>{fruit.name}</li>)}
</ul>
);Поміняй місцями fruits[0] і fruits[1]: BadList перерендерить усі <li> після зміни. GoodList просто переставить два вузли.
Чому порівняння за позицією ламається
Reconciliation React будує два дерева (попередній і наступний virtual DOM) і шукає мінімальну кількість DOM-операцій. Для списків без key елементи зіставляються за індексом у масиві. Це працює, поки порядок не змінюється.
Додай елемент на початок списку. Одразу кожен індекс зсувається на одиницю. React бачить "нові" елементи на кожній позиції і перерендерить їх усіх. Будь-який стан інпуту, фокус або позиція скролу всередині цих компонентів зникають.
Зі стабільними key React спочатку читає ключ і зіставляє старі fiber-вузли з новими за ідентичністю. Монтуються та демонтуються лише справді нові або видалені вузли.
Коли потрібні стабільні key
- Статичний список без перестановок: index підійде.
- Динамічний список з додаванням, видаленням або сортуванням: ID з бази даних або інший стабільний ідентифікатор.
- Елементи з локальним станом (поля вводу, чекбокси, панелі, що розгортаються): завжди стабільні key, інакше стан перестрибує на неправильний компонент.
- Вкладені списки: key на кожному рівні
.map(), а не лише на зовнішньому.
Якось бачив кошик у e-commerce, де кількість товарів скидалась до 1 щоразу, коли сортування після акції переставляло елементи. Фікс зайняв три символи: key={index} стало key={item.variantId}.
Типові помилки
Index як key у динамічному списку
// Перестановка елементів → стан і фокус стрибають на неправильні компоненти
{items.map((item, index) => <li key={index}>{item.text}</li>)}Індекс змінюється разом із масивом. React сприймає кожен елемент після зміни як інший компонент. Фікс: key={item.id}.
Неунікальні key
// Два користувачі "John" → React бере першого, другий демонтується
{users.map(user => <li key={user.name}>{user.name}</li>)}Key мають бути унікальними серед сусідніх елементів. Використовуй справжній ID або композитний ключ: key={user.id + '-' + user.name}.
Випадкові key на кожному рендері
// Math.random() → новий key кожен рендер → повний remount щоразу
{items.map(item => <li key={Math.random()}>{item.text}</li>)}Генеруй стабільні ID один раз під час першого завантаження даних і зберігай у стані. Ніколи не генеруй всередині .map().
Відсутні key у вкладених списках
{categories.map(cat => (
<div key={cat.id}>
{cat.items.map(item => <span>{item}</span>)} {/* key тут відсутній */}
</div>
))}Кожен .map() потребує key, а не тільки зовнішній рівень.
Де використовується
- Адмін-дашборди з рядками таблиць: ID рядків як key.
- Material-UI DataGrid: проп
getRowIdреалізує ту саму ідею на рівні компонента. - React Native
FlatList: пропkeyExtractorвиконує ту саму роль. Невірні key призводять до візуальних артефактів під час скролу у віртуалізованому списку. - TanStack Table з нескінченним скролом: стабільні key запобігають появі дублікатів при повторному завантаженні.
Питання на співбесіді
Q: Чому не використовувати index завжди?
A: Для списку, який ніколи не змінює порядок і без стану всередині елементів, index підходить. Щойно з'являється сортування, фільтрація або додавання на початок, index ламає відстеження стану.
Q: Що робити, якщо в даних немає унікального ID?
A: Генеруй його за допомогою crypto.randomUUID() або бібліотеки uuid під час першого завантаження даних і зберігай у стані. Ніколи не генеруй всередині .map().
Q: Як key впливає на хуки всередині елементів списку?
A: Key не змінює порядок виклику хуків. Але якщо key змінився, React демонтує й повторно монтує компонент, скидаючи весь стан хуків. Це корисна техніка: зміна key є найчистішим способом примусово скинути компонент.
Q: Який алгоритм React використовує для порівняння списків?
A: Reconciler знаходить найдовшу зростаючу підпослідовність (longest increasing subsequence) key, щоб мінімізувати кількість переміщень. Стабільні відсортовані key дають результат близький до O(n). Повністю перемішані key потрапляють у найгірший випадок. Це можна спостерігати через getSnapshotBeforeUpdate під час перестановки.
Приклади
Статичний список (index як key допустимий)
const MENU_ITEMS = ['Home', 'About', 'Contact'];
function Nav() {
return (
<nav>
{MENU_ITEMS.map((label, index) => (
// Статичний, ніколи не переставляється → index тут ок
<a key={index} href={`/${label.toLowerCase()}`}>{label}</a>
))}
</nav>
);
}Список не змінюється, тому індекс не зсувається. Стану, який можна втратити, немає.
Динамічний список з локальним станом (потрібен стабільний ID)
function CartList({ lineItems }) {
const [notes, setNotes] = React.useState({});
return (
<ul>
{lineItems.map(item => (
<li key={item.variantId}> {/* Стабільний SKU ID */}
<span>{item.name}</span>
<input
value={notes[item.variantId] || ''}
onChange={e => setNotes({ ...notes, [item.variantId]: e.target.value })}
placeholder="Додати нотатку"
/>
</li>
))}
</ul>
);
}Коли кошик переставляє елементи після акційного сортування, кожен інпут зберігає нотатку для свого товару. З key={index} нотатка третього товару опинилась би на першому після сортування.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.