Preact渲染的组件错误

Tra*_*Guy 36 javascript reactjs preact

我正在使用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)});
  }
}
Run Code Online (Sandbox Code Playgroud)

我究竟做错了什么?我需要以某种方式主动重新渲染吗?这是一个n + 1案件吗?

澄清:我的问题不在于国家的同步性.在上面的列表中,如果我选择删除"c",则状态会正确更新['a','b','d','e'],但呈现的组件是['a','b','c','d'].每次removePackage从数组中删除正确的调用时,都会显示正确的状态,但会呈现错误的列表.(我删除了这些console.log陈述,所以看起来它们不是我的问题).

Jas*_*ler 80

这是Preact的文档完全没有提供的经典问题,所以我想亲自为此道歉!如果有兴趣,我们一直在寻找帮助撰写更好的文档.

这里发生的是你使用数组的索引作为键(在渲染中的地图中).这实际上只模拟了VDOM diff在默认情况下的工作方式 - 键总是0-nn数组长度的位置,因此删除任何项只会将最后一个键从列表中删除.

说明:键超越渲染

在您的示例中,想象一下(虚拟)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>
Run Code Online (Sandbox Code Playgroud)

现在,当我们在列表中的第二个项目上单击"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>
Run Code Online (Sandbox Code Playgroud)

由于VDOM库只知道你在每个渲染上给出它的新结构(而不​​是如何从旧结构改为新结构),所以键完成后基本上告诉它项目01保持原位 - 我们知道这个是不正确的,因为我们希望1删除索引处的项目.

请记住:key优先于默认的子差异重新排序语义.在这个例子中,因为key它总是只是基于0的数组索引,所以最后一个item(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>
  );
});
Run Code Online (Sandbox Code Playgroud)

哇,这是我原本想要的那种啰嗦.

TL,DR:从不使用数组索引(迭代索引)key.它充其量只是模仿默认行为(自上而下的子重新排序),但更常见的是它只是将所有差异推到最后一个孩子身上.


编辑: @tommy推荐这个eslint-plugin-react文档的优秀链接,它比我上面做的更好地解释它.

  • 谢谢!这就是答案.伙计,我尝试了很多`setState`和事件处理程序的迭代,甚至从未看过`key` :) (3认同)
  • 不,它实际上也是一样的. (2认同)
  • @TJCrowder 是的,它适用于 React 和任何其他使用键来跟踪唯一项目的 VDOM 风格的实现。 (2认同)