awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Реконсиляция: эвристики диффа и ловушка ключей

Суть React сводит O(n³) дифф деревьев к O(n) двумя эвристиками — одинаковый тип сохраняет состояние, разный тип уничтожает его. Ключи идентифицируют элементы списка между рендерами: индексные ключи молча портят неуправляемое состояние при перестановке.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Чекбокс, отмеченный в строке 3, остаётся отмеченным после удаления строки 1 — теперь на том месте, что было строкой 4. Никакого предупреждения React. Никакой ошибки в консоли. Одно изменение — key={index} на key={item.id} — и баг исчезает. Ключи — не подсказки. Это механизм, которым React сопоставляет файберы с элементами между рендерами, и если ошибиться — состояние молча портится.

Реконсиляция: правила диффа. Реконсиляция сравнивает новое дерево элементов с текущим fiber-деревом, чтобы решить, что сохранить, обновить или заменить. Полный дифф дерева — O(n³) в общем случае; React делает его O(n) двумя эвристиками.

Эвристика 1 — одинаковый тип, тот же fiber. Если <div> остаётся <div> или <ComponentA> остаётся <ComponentA>, React обновляет существующий fiber на месте — патчит props, запускает реконсиляцию для потомков. Fiber сохраняет своё состояние.

Эвристика 2 — разный тип, полная замена. Если <div> становится <span> или <ComponentA> становится <ComponentB>, React сносит всё поддерево (размонтирует его, уничтожает состояние) и строит новое с нуля. Он не пытается делать дифф через границу типов. Именно поэтому условный рендеринг двух разных компонентов в одном слоте сбрасывает всё состояние — даже если новый компонент выглядит похоже.

Ключи идентифицируют дочерние элементы списка между рендерами. В списке React сопоставляет старые и новые дочерние элементы по их key, а не по позиции. Стабильный ключ говорит React: «это тот же логический элемент, даже если он переместился». В этом и есть весь смысл ключей. React строит карту из key в старый fiber, затем обходит новые дочерние элементы, сопоставляя каждый со старым fiber по ключу. Сопоставленный fiber несёт вперёд всё статeful: значения useState, содержимое useRef, неуправляемое состояние DOM (введённый текст input, отмеченность checkbox, позицию скролла, фокус) и сам DOM-узел.

key={index} vs key={id} — что происходит при удалении
key={index} (сломано)
До: key=0 → элемент А ✓отмечен
До: key=1 → элемент Б
До: key=2 → элемент В
Удаляем элемент А
После: key=0 → элемент Б (но fiber от А — ✓отмечен!)
После: key=1 → элемент В
key={item.id} (правильно)
До: key=“a” → элемент А ✓отмечен
До: key=“b” → элемент Б
До: key=“c” → элемент В
Удаляем элемент А
После: key=“b” → элемент Б (правильный fiber, без состояния)
После: key=“c” → элемент В

Почему key={index} — ловушка. С key={index} ключ — это позиция, поэтому при перестановке списка или вставке элемента в начало ключ каждого элемента смещается. React тогда считает, что элемент-по-индексу-0 — «тот же элемент», хотя данные переместились — он сохраняет состояние и DOM старого fiber, но передаёт ему props нового элемента. Видимый баг: чекбокс, отмеченный в строке 3, остаётся отмеченным после удаления строки 1, теперь прикреплённый к тому, что было строкой 4.

Управляемые props обновляются корректно (они передаются заново при каждом рендере); неуправляемое состояние — нет (оно живёт на fiber). key={index} безопасен только для списка, который статичен и никогда не переупорядочивается; для любого динамического списка он молча портит состояние.

Граничные случаи

Math.random() в качестве ключа хуже, чем key={index}. Это присваивает новый ключ при каждом рендере, поэтому React всегда видит «другой» элемент на каждой позиции — он размонтирует и перемонтирует каждый элемент списка при каждом обновлении. Вы получаете правильное состояние (потому что fiber всегда новый), но платите за размонтирование + монтирование каждого элемента при каждом нажатии клавиши. key={index} неправильный для динамических списков; key={Math.random()} неправильный для всех списков.

Викторина

Список элементов рендерится с `key={index}`. Вы удаляете первый элемент. Чекбокс, который был отмечен на старом втором элементе, теперь отмечен на неправильной строке. Почему?

Викторина

Вы рендерите `condition ? <ComponentA /> : <ComponentB />` в одном слоте. Когда условие меняется, что происходит с состоянием ComponentA?

Викторина

Почему управляемое состояние (value, checked, переданные как props) выглядит правильно с `key={index}`, а неуправляемое (введённый текст, позиция скролла) — нет?

Вспомните перед уходом
  1. 01
    Объясните на уровне fiber, почему key={index} портит состояние при перестановке списка.
  2. 02
    Что делает React, когда тип компонента меняется в одном слоте?
  3. 03
    Когда key={index} действительно безопасен?
Итог

Реконсиляция сводит O(n³) дифф деревьев к O(n) двумя эвристиками: одинаковый тип элемента означает обновление fiber на месте (состояние сохраняется); разный тип означает снос поддерева и перестройку с нуля (состояние уничтожается). Для списков React использует key для сопоставления новых дочерних элементов со старыми файберами по идентичности, а не позиции. Fiber несёт всё stateful — значения useState, рефы, неуправляемое состояние DOM — поэтому сопоставление неправильного fiber с неправильным элементом молча портит это состояние. key={index} — ловушка: индексные ключи позиционные, поэтому любая перестановка, вставка или удаление смещает, какой fiber достанется элементу. Используйте стабильный ключ идентичности (id из базы данных, slug) всегда, когда список динамический.

Связанные уроки
встречается в143
Продолжить восхождение ↑Приоритетные lanes, time-slicing и useTransition
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.