Головна / Статті

Ключі React як ідентичність компонента: збереження та скидання стану

Ключі React визначають, яка інстанція компонента залишиться після відображення. Як індексні ключі пропускають стан між рядками та як зміна ключа навмисно скидає форму.

Видавець: REACTAPP.TOP6 хв читання1462 слів

Джерела: React Keys Solve a Much Stranger Problem Than Rendering Lists

Більшість розробників React додають key до кожного виклику .map() без особливих роздумів, а більшість пояснень обмежуються фразою «React потребує його для відстеження змінених елементів». Ця відповідь є правильною, але неповною. Насправді React на кожному рендерингу запитує, чи є елемент, який він бачить зараз, тим самим, що й минулого разу, а key — це спосіб відповісти на це запитання. Як тільки ви почнете розглядати key як механізм ідентифікації, помилки з index-key стають передбачуваними, а ви отримуєте чіткий, декларативний спосіб скидання стану компонента за потреби.

Рефлексивна версія

Ось шаблон, який міститься майже в кожному кодбазі. Кожен користувач у масиві стає елементом User, а ідентифікатор користувача використовується як key:

users.map((user) => (
  <User key={user.id} user={user} />
))

У цьому фрагменті немає жодних проблем. Цікавою є ситуація, коли ключ обирається неправильно, і це проявляється лише тоді, коли компоненти мають власний стан.

Компонент із станом робить проблему видимою

Розгляньмо компонент-карту, який зберігає локальний флаг selected. Цей флаг знаходиться всередині компонента, а не у властивості user, тому React мусить вирішувати, яка інстанція карти володіє яким флагом під час кожного оновлення:

interface UserCardProps {
  user: User;
}

const UserCard = ({ user }: UserCardProps) => {
  const [selected, setSelected] = useState(false);

  return (
    // ...
  );
}

Тепер відобразимо трьох людей: Еліс, Боба та ще одного користувача. У вигляді пар індекс-елемент масив виглядає так:

0 → Alice
1 → Bob
2 → Maxime

Що бачить React з індексними ключами

Загальною порадою є уникати використання індексів масиву як ключів. Причина цього стає зрозумілою, якщо подивитися на елементи, які насправді отримує React при використанні індексу:

<User key={0} user={alice} />
<User key={1} user={bob} />
<User key={2} user={maxime} />

Припустимо, що список підтримує видалення, додавання елементів чи їх перестановку шляхом перетягування. Якщо видалити Алісу, наступне відображення буде таким:

<User key={0} user={bob} />
<User key={1} user={maxime} />

З вашої точки зору, Аліса зникла, а двоє інших перемістилися вище. Боб перейшов з індексу 1 на індекс 0, а третій користувач — з 2 на 1. З точки зору React ніщо не свідчить про це. Він бачить, що елемент із key={0} існував раніше та все ще існує, причому має той самий тип компонента. Такий самий тип разом із таким самим ключем — це саме той сигнал, який React використовує для вирішення, чи потрібно зберегти певну інстанцію. Тож він зберігає інстанцію, яка раніше відображала Алісу, та передає їй параметри Боба; зберігає другу інстанцію та передає їй параметри третього користувача, а третю інстанцію видаляє.

Наслідок: якщо була обрана картка Аліси, тепер обраною виглядає картка Боба, оскільки стан selected належав екземпляру з ключем 0, а не Алісі. Такий самий вид витоку впливає на неконтрольовані дані вхіду, фокус, положення прокрутки всередині рядка та будь-які інші стани, пов’язані з екземпляром.

Чому ці баги прослизають повз

Неправильні ключі дивовижно поширені у реальних застосунках будь-якого розміру. Список, до якого постійно додають елементи та який ніколи не змінює порядку, поводиться однаково як з індексними ключами, так і з стабільними ключами, тож нічого не виглядає не так. Баг проявляється пізніше, коли хтось додає функції видалення, фільтрації, сортування чи переупорядкування. Инструменти перевірки допомагають: правило react/no-array-index-key з пакета eslint-plugin-react позначає індексні ключі, щоб рішення приймалося свідомо, а не за звичкою.

Коли елемент справді не має стабільного ідентифікатора, його потрібно створити під час створення даних (наприклад, за допомогою crypto.randomUUID() під час вставки), а не під час відображення. Ключ, створений всередині .map(), змінюється при кожному відображенні та змушує React перезавантажувати кожен рядок, що гірше, ніж використання індексу.

Props описують вміст, ключі — ідентичність

Тут допоможе компактна ментальна модель:

  • props відповідають на запитання "що зараз міститься в цьому елементі?"
  • ключ відповідає на запитання "що це за елемент?"

Props можуть вільно змінюватися, тоді як компонент залишається тією самою інстанцією з недоторканим станом. Якщо рядок підтримує безпосередню редагування, зміна імені користувача не повинна змусити React видаляти рядок та починати все знову; це все одно той самий користувач. Стабільний ключ, такий як user.id, саме це й виражає.

Навмисне створення нової інстанції

Іноді потрібно протилежне: ви хочете, щоб React сприймав компонент як абсолютно новий. Ключ також працює поза списками:

<Profile key={user.id} user={user} />

Уявіть, що Profile зберігає незбережений чернеток у локальному стані:

interface ProfileProps {
  user: User;
}

const Profile = ({ user }: ProfileProps) => {
  const [draft, setDraft] = useState({...});
}

Якщо батьківський елемент змінюється з одного користувача на іншого, перенесення чернетка попереднього користувача до наступного профілю стане справжньою помилкою, а можливо, навіть причиною витоку даних. Без ключа React бачить один і той самий тип компонента в тій самій позиції та зберігає інстанцію, тож чернеток залишається. З key={user.id} новий ідентифікатор повідомляє React, що старий Profile більше не існує. Він демонтує стару інстанцію, видаляє її стан, виконує процедури очищення ефектів та монтує нову, у якій ініціалізатори useState виконуються знову.

Це ідіоматична альтернатива написанню ефекту, який стежить за user та вручну скидає кожен елемент стану. Такий підхід з використанням ефектів спочатку відображає старий, застарілий стан, перш ніж його виправити, і легко забути про певне поле. Щоб дізнатися більше про те, чому синхронізація стану через ефекти часто призводить до проблем, перегляньте безпечніший підхід, який замінює синхронізацію на основі useEffect.

Скидання форми за допомогою ключа

Цей самий трик дозволяє чисто скидати форми:

<CreateUserForm key={draft.id} />

Ключ виступає як межа ідентичності. Кожного разу, коли змінюється draft.id, наприклад після успішної передачі даних, що створює нову чернетку, React створює новий елемент CreateUserForm без залишків значень полів, помилок валідації чи позначок про зміни.

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

Де насправді знаходиться стан

Усе це випливає з того, як React зберігає стан. Стан не прив’язаний до функції, яку ви написали; якби це було так, кожен елемент <User /> мав би спільне значення selected. Натомість React пов’язує стан із певною позицією у відтвореному дереві. Під час кожного відтворення він аналізує новий результат та вирішує для кожного елемента, чи є він продовженням чогось з попереднього відтворення. Тип компонента та його позиція є частиною цього рішення, а ключ уточнює його серед братських елементів. Стаття де useState зберігає своє значення детальніше розглядає структуру fiber, яка лежить в основі цього процесу.

Правило, яке з цього випливає, є простим:

  • однаковий тип, однакова позиція, однаковий ключ: React розглядає це як один і той самий компонент та зберігає його стан
  • інший тип або інший ключ: React видаляє старий стан та створює нову інстанцію
  • Списки — це просто перше місце, де позиція вже не є надійною. Коли елементи можуть переміщуватися, з’являтися чи зникати, вираз „друга дитина“ більше не означає „те саме, що й минулого разу“, тому React потребує від вас вказання ідентифікатора. Стабільний ключ дозволяє йому відстежувати елемент навіть під час змін списку; змінений ключ є прямим сигналом про те, що на його місце прийшов щось нове.

    Основні висновки

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