Skip to main content

Чому в React потрібен ключ?

key є спеціальним пропом React, який унікально ідентифікує елементи списку, щоб алгоритм reconciliation (узгодження) міг відстежувати додавання, видалення та перестановки без повного перебудування DOM.

Теорія

TL;DR

  • Key схожий на бейдж на конференції: React використовує його, щоб зіставити "старий елемент" з "новим" між рендерами, а не перевіряти всіх за позицією в черзі.
  • Без key React порівнює по індексу. Будь-яка перестановка змушує оновити кожен елемент після зміни.
  • Зі стабільними key React зіставляє по ідентичності та оновлює лише те, що реально змінилось.
  • Правило вибору: статичний список без перестановок, тоді index підійде. Динамічний список із перестановками або локальним станом, тоді потрібен стабільний унікальний ID.

Короткий приклад

jsx
// Без 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 у динамічному списку

jsx
// Перестановка елементів → стан і фокус стрибають на неправильні компоненти {items.map((item, index) => <li key={index}>{item.text}</li>)}

Індекс змінюється разом із масивом. React сприймає кожен елемент після зміни як інший компонент. Фікс: key={item.id}.

Неунікальні key

jsx
// Два користувачі "John" → React бере першого, другий демонтується {users.map(user => <li key={user.name}>{user.name}</li>)}

Key мають бути унікальними серед сусідніх елементів. Використовуй справжній ID або композитний ключ: key={user.id + '-' + user.name}.

Випадкові key на кожному рендері

jsx
// Math.random() → новий key кожен рендер → повний remount щоразу {items.map(item => <li key={Math.random()}>{item.text}</li>)}

Генеруй стабільні ID один раз під час першого завантаження даних і зберігай у стані. Ніколи не генеруй всередині .map().

Відсутні key у вкладених списках

jsx
{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 допустимий)

jsx
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)

jsx
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} нотатка третього товару опинилась би на першому після сортування.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?