Обновление DOM при индексах в качестве ключей
Что произойдет в реальном DOM после нажатия на кнопку "rotate" в данном React-компоненте?
import React, { useState } from 'react';
function SimpleList(props) {
const [list, setList] = useState(props.list || ['a', 'b']);
const rotateList = () => {
setList([...list.reverse()]);
};
return (
<div>
{list.map((txt, id) => (
<div key={id}>{txt}</div>
))}
<button onClick={rotateList}>rotate</button>
</div>
);
}
Компонент будет вызываться так:
<SimpleList list={null} />
Для понимания результата необходимо знать, как React сравнивает предыдущий и следующий рендер (reconciliation) и как используется key в списках. React сопоставляет элементы списка между рендерами по key; если key совпал, React считает, что это “тот же” элемент и пытается обновить его содержимое, а не создавать новый узел.
Алгоритм согласования и виртуальный DOM
При setState/setList React строит новое дерево элементов (виртуальное представление), сравнивает его с предыдущим и применяет минимальные изменения к реальному DOM. Минимальность достигается тем, что React старается:
- Переиспользовать существующие DOM-узлы, если считает, что это те же элементы.
- Обновлять только то, что реально изменилось (например, текстовый узел).
Роль атрибута key в списках
key — это идентификатор элемента списка между рендерами. Корректный key должен быть:
- Стабильным (не меняться от рендера к рендеру для одного и того же “логического” элемента).
- Уникальным среди соседей в списке.
key={id} где id — индекс) подходит только для статических списков без перестановок/вставок/удалений. При изменении порядка это приводит к тому, что React “путает” элементы: DOM-узлы остаются на местах, а данные внутри них меняются.Детальный разбор процесса
Компонент вызывается так:
<SimpleList list={null} />
Значит начальное состояние берётся из значения по умолчанию:
const [list, setList] = useState(props.list || ['a', 'b']);
Поскольку props.list равен null, выражение props.list || ['a','b'] даёт ['a','b']. Первый рендер создаёт элементы:
<div key={0}>a</div>
<div key={1}>b</div>
Далее нажимается кнопка rotate:
const rotateList = () => {
setList([...list.reverse()]);
};
Ключевое здесь:
reverse()мутирует исходный массив, то есть меняет порядок элементов внутри того же объекта массива.[...list.reverse()]создаёт новый массив, но уже из перевёрнутого исходного. Итоговое новое состояние по значениям:['b','a'].
Следующий рендер вернёт:
<div key={0}>b</div>
<div key={1}>a</div>
Теперь важно, как React сопоставит “старое” и “новое”:
- Элемент с
key={0}был и остался, значит DOM-узел для первогоdivпереиспользуется, а меняется только текст:a → b. - Элемент с
key={1}был и остался, значит DOM-узел для второгоdivпереиспользуется, а меняется только текст:b → a.
То есть физического переставления двух div местами в DOM не происходит, потому что с точки зрения ключей порядок не “переехал”: ключ 0 как был в первой позиции, так и остался в первой позиции, и аналогично ключ 1.
Почему не вариант 2
Полное удаление и создание новых div обычно происходит, когда React не может сопоставить элементы (например, ключи изменились или отсутствуют, и структура/типы элементов не совпадают). Здесь ключи совпадают (0 и 1), поэтому выгоднее обновить текст внутри существующих узлов.
Почему не вариант 3
Виртуальный DOM не означает “реальный DOM не меняется”. Наоборот: виртуальное сравнение нужно, чтобы правильно и эффективно обновить реальный DOM. Здесь данные изменились, значит изменения попадут и в реальный DOM.
Почему не вариант 4
Чтобы React “физически” переставил DOM-узлы, нужно, чтобы идентичности элементов были привязаны к данным (например, key="a" и key="b"), а не к позициям. При key={index} идентичность привязана к месту, поэтому React обновляет содержимое, а не меняет порядок узлов.
Как сделать корректную “ротацию” визуально
Если элементы списка имеют уникальные значения, ключ можно привязать к значению:
{list.map((txt) => (
<div key={txt}>{txt}</div>
))}
Тогда при переходе ['a','b'] → ['b','a'] React будет видеть, что элемент с ключом b должен оказаться первым, а элемент с ключом a — вторым, и сможет корректно отразить перестановку (в терминах reconciliation это будет переупорядочивание по ключам).
reverse() мутирует массив. Для предсказуемости состояния обычно следует избегать мутаций и делать немутирующие преобразования.Например, без мутации:
const rotateList = () => {
setList(prev => [...prev].reverse());
};
При key={index} React считает элементы с одинаковыми индексами “теми же” элементами между рендерами, поэтому после reverse() два div в DOM остаются на своих местах, а меняются только их текстовые узлы: первый становится 'b', второй — 'a'.