Подтвердить что ты не робот

Неверные компоненты, предоставленные Preact

Я использую Preact (для всех целей и задач, React), чтобы отобразить список элементов, сохраненных в массиве состояний. У каждого элемента есть кнопка удаления рядом с ним. Моя проблема: при нажатии кнопки правильный элемент удаляется (я проверил это несколько раз), но элементы повторно отображаются с отсутствующим элементом last, а удаленный все еще есть. Мой код (упрощенный):

import { h, Component } from 'preact';
import Package from './package';

export default class Packages extends Component {
  constructor(props) {
    super(props);
    let packages = [
      'a',
      'b',
      'c',
      'd',
      'e'
    ];
    this.setState({packages: packages});
  }

  render () {
    let packages = this.state.packages.map((tracking, i) => {
      return (
        <div className="package" key={i}>
          <button onClick={this.removePackage.bind(this, tracking)}>X</button>
          <Package tracking={tracking} />
        </div>
      );
    });
    return(
      <div>
        <div className="title">Packages</div>
        <div className="packages">{packages}</div>
      </div>
    );
  }

  removePackage(tracking) {
    this.setState({packages: this.state.packages.filter(e => e !== tracking)});
  }
}

Что я делаю неправильно? Нужно ли мне как-то активно пересматривать? Является ли это случаем n + 1 как-то?

Разъяснение. Моя проблема заключается не в синхронности состояния. В приведенном выше списке, если я выбираю удалить 'c', состояние корректно обновляется до ['a','b','d','e'], но отображаемые компоненты ['a','b','c','d']. При каждом вызове removePackage правильный шаблон удаляется из массива, отображается правильное состояние, но отображается неправильный список. (Я удалил инструкции console.log, так что это не похоже, что это моя проблема).

4b9b3361

Ответ 1

Это классическая проблема, которая полностью недоказана документацией Preact, поэтому я хотел бы лично извиниться за это! Мы всегда ищем помощь при написании лучшей документации, если кто-то заинтересован.

Что здесь произошло, так это то, что вы используете индекс своего массива в качестве ключа (на вашей карте в рендере). Это фактически просто эмулирует, как работает VDOM diff по умолчанию - ключи всегда 0-n, где n - длина массива, поэтому удаление любого элемента просто удаляет последний ключ из списка.

Объяснение: Ключи transcend render

В вашем примере представьте, как (виртуальная) DOM будет выглядеть на начальной визуализации, а затем после удаления элемента "b" (индекс 3). Ниже приведем вид, что ваш список состоит всего из 3 предметов (['a', 'b', 'c']):

Вот то, что производит исходный рендеринг:

<div>
  <div className="title">Packages</div>
  <div className="packages">
    <div className="package" key={0}>
      <button>X</button>
      <Package tracking="a" />
    </div>
    <div className="package" key={1}>
      <button>X</button>
      <Package tracking="b" />
    </div>
    <div className="package" key={2}>
      <button>X</button>
      <Package tracking="c" />
    </div>
  </div>
</div>

Теперь, когда мы нажимаем "X" во втором элементе списка, "b" передается на removePackage(), который устанавливает state.packages в ['a', 'c']. Это вызывает наш рендер, который создает следующую (виртуальную) DOM:

<div>
  <div className="title">Packages</div>
  <div className="packages">
    <div className="package" key={0}>
      <button>X</button>
      <Package tracking="a" />
    </div>
    <div className="package" key={1}>
      <button>X</button>
      <Package tracking="c" />
    </div>
  </div>
</div>

Поскольку библиотека VDOM знает только о новой структуре, которую вы даете ей для каждого рендеринга (а не как перейти от старой структуры к новой), то, что сделали ключи, в основном говорит, что элементы 0 и 1 остался на месте - мы знаем, что это неверно, потому что мы хотели удалить элемент с индексом 1.

Помните: key имеет приоритет над семантикой переопределения дочерних различий по умолчанию. В этом примере, поскольку key всегда является только индексом массива на основе 0, последний элемент (key=2) просто выпадает, потому что он отсутствует в последующем рендере.

Исправление

Итак, чтобы исправить ваш пример - вы должны использовать что-то, что идентифицирует элемент, а не его смещение как ваш ключ. Это может быть сам элемент (любое значение приемлемо в качестве ключа) или свойство .id (предпочтительнее, поскольку оно позволяет избежать ссылок на рассеивающие объекты вокруг которых может предотвратить GC):

let packages = this.state.packages.map((tracking, i) => {
  return (
                                  // ↙️ a better key fixes it :)
    <div className="package" key={tracking}>
      <button onClick={this.removePackage.bind(this, tracking)}>X</button>
      <Package tracking={tracking} />
    </div>
  );
});

Ну, это было гораздо более длинным, чем я предполагал.

TL, DR: никогда не используйте индекс массива (индекс итерации) как key. В лучшем случае он имитирует поведение по умолчанию (перераспределение дочернего элемента вниз), но чаще всего он просто толкает все, отличные от последнего ребенка.


изменить: @tommy рекомендуется этот отличный ссылку на документы, посвященные eslint-plugin-react, которые лучше объясняют это, чем я сделал выше.