Обновление 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()]);
};

Ключевое здесь:

  1. reverse() мутирует исходный массив, то есть меняет порядок элементов внутри того же объекта массива.
  2. [...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'.