Preact 渲染的错误组件

IT技术 javascript reactjs preact
2021-04-02 00:22:01

我正在使用 Preact(出于所有意图和目的,React)来呈现保存在状态数组中的项目列表。每个项目旁边都有一个删除按钮。我的问题是:当点击按钮时,正确的项目被删除(我验证了几次),但项目重新渲染,最后一个项目丢失,删除的项目仍然存在。我的代码(简化):

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陈述,所以看起来它们不是我的问题)。

1个回答

这是一个经典的问题,Preact 的文档完全没有提供服务,所以我想个人为此道歉!如果有人感兴趣,我们一直在寻求帮助来编写更好的文档。

这里发生的事情是您使用数组的索引作为键(在渲染中的地图中)。这实际上只是一个模拟的虚拟域差异是如何工作的默认值-键始终0-n哪里n是数组的长度,因此删除任何项目简单地脱落列表中的最后关键。

说明:键超越渲染

在您的示例中,想象一下(虚拟)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 库只知道你在每次渲染时给它的新结构(不知道如何从旧结构更改为新结构),键所做的基本上是告诉它项目01保持原位 - 我们知道这一点不正确,因为我们希望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 docs,它比我上面做的更好解释它。

不,实际上两者都是一样的。
2021-05-29 00:22:01
是否仅在 Preact 中会导致这种渲染问题?我在 React 上从来没有遇到过这些问题。
2021-05-30 00:22:01
嗨@JasonMiller您可以提供,以模拟不正确使用时,如何一些基本的例子index,请...我在这里试图gist.github.com/jurosh/5ef25a18ac3f6b21355ca6a4dd719c55,但不能得到错误的行为,看起来像使用indexkey是安全的。真的不只是Preact吗?
2021-06-07 00:22:01
谢谢!那就是答案。伙计,我尝试了很多迭代setState和事件处理程序,甚至从未看过key:)
2021-06-16 00:22:01
这是安全的,但通常是不正确的。您只会在渲染带有状态的 DOM 元素时看到影响,例如其中包含用户输入文本的输入。它与 Preact 无关,这只是键的工作方式。索引键强制执行有序差异,这通常不是最好的差异类型。
2021-06-19 00:22:01