Удаление элемента приводит к тому, что React удаляет последний DOM node, а не тот, который связан с этим элементом

Я пытался анимировать вставку и удаление списка с помощью ReactCSSTransitionGroup, но анимация удаления всегда анимирует только последний элемент списка, а не тот, который удаляется.

Здесь jsbin, чтобы проиллюстрировать эту проблему. Попробуйте нажать кнопку "Добавить", чтобы убедиться, что анимация вставки действительно работает так, как ожидалось, затем нажмите "x", кроме любого элемента, чтобы увидеть проблему, когда анимированный последний элемент списка вместо того, который вы пытались удалить.

Я сделал что-то неправильно во время настройки TransitionGroup или я что-то упустил в определениях перехода CSS?

Ответ 1

У вас возникла эта проблема, потому что вы используете index в качестве своего ключа:

let nodes = items.map((item, index) => {
  let idx = index
  return (<Item key={index} value={item} index={index} _delete={this._onDelete}/>)
})

React использует свойство key во время виртуальной DOM, чтобы выяснить, какой элемент был удален, но индексы никогда не будут служить этой цели достаточно.

Рассмотрим этот пример: вы начинаете со следующего массива, что приводит к следующей структуре DOM:

const arr = [2, 4, 6, 8];

<li key={0}>2</li>
<li key={1}>4</li>
<li key={2}>6</li>
<li key={3}>8</li>

Тогда представьте, что вы удаляете элемент по индексу 2. Теперь у вас есть следующий массив и следующая структура DOM:

const arr = [2, 4, 8];

<li key={0}>2</li>
<li key={1}>4</li>
<li key={2}>8</li>

Обратите внимание, что 8 теперь находится в индексе 2; React видит, что разница между этой структурой DOM и последней состоит в том, что отсутствует li с ключом 3, поэтому он удаляет его. Таким образом, независимо от того, какой элемент массива вы удалили, в результате структуры DOM будет отсутствовать li с ключом 3.

Решение состоит в использовании уникального идентификатора для каждого элемента в списке; в реальном приложении у вас может быть поле id или какой-либо другой первичный ключ; для такого приложения, как этот, вы можете сгенерировать инкрементирующий идентификатор:

let id = 0;
class List extends Component {
  constructor() {
    this.state = {
      items: [{id: ++id, value: 1}, {id: ++id, value: 2}]
    }

    // ...
  }

  _onClick(e) {
    this.state.items.push({id: ++id, value: Math.round(Math.random() * 10)})
    this.setState({items: this.state.items})
  }

  // ...

  render() {
    let items = this.state.items
    let nodes = items.map((item, index) => {
      let idx = index
      return (<Item key={item.id} value={item.value} index={index} _delete={this._onDelete}/>)
    })

    // ...
  }
}

Рабочий пример: http://jsbin.com/higofuhuni/2/edit