Опубліковано
4 хв читання

Ключі в React

Автори
  • Learna
    Ім’я
    Learna

Ключі

Ключі в React

Навіщо потрібен key, коли ми виводимо масив як список елементів? За ключем React розпізнає той самий елемент серед його сусідів після оновлення списку. Без цього легко переплутати записи: якщо додати рядок на початок, його поле введення може успадкувати текст, який користувач набирав зовсім для іншого запису.

Як обрати ключ

  • Беріть ID із даних. Часто це вже готовий ідентифікатор запису з бази.
  • Ключ має бути унікальним серед сусідніх елементів. У різних списках однакові ключі допустимі.
  • Не змінюйте ключ, поки це той самий запис. Якщо під час кожного рендерингу викликати Math.random(), React щоразу створюватиме піддерево заново, втрачаючи попередній стан і DOM-вузли.
  • Індекс масиву позначає місце у списку, а не сам запис. Він може підійти для незмінного списку, але створить проблеми, якщо рядки можна додавати, видаляти чи переставляти.

Що відбувається з ключами-індексами

Залишимо createElement із початкового прикладу й додамо поля введення, щоб побачити помилку на практиці. Виведіть компонент ListWithoutKeys у своєму React-застосунку. Впишіть нотатку в поле Item #0 й натисніть Add to top.

import { createElement, useRef, useState } from 'react';

function ListWithoutKeys() {
  const [list, setList] = useState([{ id: 0, label: 'Item #0' }]);
  const nextId = useRef(1);

  function prepend() {
    // Create identity once, when data is inserted, not during rendering.
    const id = nextId.current++;
    setList((previous) => [{ id, label: `Item #${id}` }, ...previous]);
  }

  return createElement(
    'section',
    null,
    createElement(
      'ul',
      null,
      list.map((item, index) =>
        createElement(
          'li',
          { key: index },
          createElement(
            'label',
            null,
            item.label,
            createElement('input', {
              'aria-label': `Note for ${item.label}`,
              defaultValue: '',
            }),
          ),
        ),
      ),
    ),
    createElement('button', { type: 'button', onClick: prepend }, 'Add to top'),
  );
}

Оновлення списку, у якому ключами слугують індекси

Перший рядок тепер називається Item #1, але в його полі залишилася нотатка для Item #0. React зберіг поле на позиції з ключем 0, хоча тепер тут інший запис. А Item #0 перемістився на індекс 1 і отримав інше поле. Це вже помилка в даних: текст користувача опинився біля чужого запису.

Прив’яжіть ключ до запису

Замініть індекс на item.id. У цьому демо ID створює локальний лічильник. У справжньому списку краще використати постійний ID, який уже є в даних. Якщо запис створюється на клієнті, призначайте йому ID один раз — під час додавання. Не генеруйте новий ID усередині map під час рендерингу.

import { createElement, useRef, useState } from 'react';

function ListWithKeys() {
  const [list, setList] = useState([{ id: 0, label: 'Item #0' }]);
  const nextId = useRef(1);

  function prepend() {
    // Create identity once, when data is inserted, not during rendering.
    const id = nextId.current++;
    setList((previous) => [{ id, label: `Item #${id}` }, ...previous]);
  }

  return createElement(
    'section',
    null,
    createElement(
      'ul',
      null,
      list.map((item) =>
        createElement(
          'li',
          { key: item.id },
          createElement(
            'label',
            null,
            item.label,
            createElement('input', {
              'aria-label': `Note for ${item.label}`,
              defaultValue: '',
            }),
          ),
        ),
      ),
    ),
    createElement('button', { type: 'button', onClick: prepend }, 'Add to top'),
  );
}

Оновлення списку зі стабільними ключами

Повторіть ті самі дії. Новий рядок отримає порожнє поле, а біля Item #0 залишиться те саме поле з вашою нотаткою. Позиції записів змінилися, але їхні ключі — ні.

Це не означає, що React викличе лише функцію нового компонента. Повторний рендеринг, зміни в DOM і монтування заново — різні речі. Коли батьківський компонент оновлюється, React може знову викликати дочірні компоненти, зберігши їхній стан і DOM-вузли. Мемоізація — окрема тема оптимізації. Ключі насамперед потрібні, щоб React не плутав елементи.

Навіщо React потрібні ключі?

Уявіть, що файли на робочому столі можна називати лише за порядком: перший, другий, третій. Видалили перший — і всі назви зсунулися. З постійними іменами такої плутанини немає. Ключі виконують схожу роль у списку сусідніх елементів. В іншому списку ті самі ключі можна використати знову. Водночас React враховує тип компонента й його місце в дереві: самого ключа недостатньо, щоб зберегти стан після перенесення компонента до іншого батька.

Коли стан треба скинути навмисно

Іноді компонент варто створити заново. Скажімо, під час перемикання профілів форма має починатися з даних нового профілю, а не з незбережених змін попереднього:

<ProfileEditor key={selectedProfile.id} profile={selectedProfile} />

Це фрагмент для вбудовування у ваш застосунок: компонент ProfileEditor і змінна selectedProfile мають бути визначені в ньому. Коли ID зміниться, React змонтує редактор заново й скине його локальний стан. Використовуйте це свідомо, щоб випадково не втратити незбережені зміни під час кожного рендерингу.

key обробляє сам React — компонент не отримає його як звичайний проп. Якщо компоненту потрібен ID, передайте ще й окремий проп id. useId призначений для зв’язування атрибутів доступності, а не для створення ключів списку.

Якщо одному запису відповідає кілька елементів без спільної обгортки, використайте повний запис Fragment із ключем. Короткому запису <>...</> передати key не можна.

Перевірте самі

  1. Виведіть один із повних прикладів у локальному React-проєкті.
  2. Введіть нотатку й додайте рядок на початок. Подивіться, біля якого запису лишився текст.
  3. Повторіть зі стабільними ID: нотатка має залишитися біля Item #0.

Початковий приклад у CodeSandbox можна відкрити окремо. Сторонній iframe більше не завантажується разом зі статтею.

Джерела