我正在使用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语句,因此看起来好像不是我的问题)。
['a','b','d','e']
['a','b','c','d']
removePackage
console.log
这是一个经典的问题,Preact的文档完全没有解决这个问题,因此我想为此道歉!如果有人感兴趣,我们一直在寻求帮助以编写更好的文档。
这里发生的事情是您将Array的索引用作键(在render中的地图中)。这实际上只是一个模拟的虚拟域差异是如何工作的默认值- 键始终0-n哪里n是数组的长度,因此删除任何项目简单地脱落列表中的最后关键。
0-n
n
在您的示例中,想象一下(虚拟)DOM在初始渲染中的外观,然后在删除项目“ b”(索引3)之后的外观。在下面,让我们假设您的列表只有3个项目(['a', 'b', 'c']):
['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:
removePackage()
state.packages
['a', '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="c" /> </div> </div> </div>
由于VDOM库只知道您在每个渲染器上赋予它的新结构(而不是如何从旧结构更改为新结构),因此键所做的基本上是告知项目0并1保留在原位- 我们知道是错误的,因为我们希望1删除索引处的项目。
0
1
切记:key优先于默认的子diff重新排序语义。在此示例中,由于key始终只是基于0的数组索引,因此最后一项(key=2)会被丢弃,因为它是后续渲染中缺少的一项。
key
key=2
因此,要修正您的示例-您应该使用可以识别 项目 而不是其 偏移量 的键作为键。这可以是项目本身(任何值都可以作为键),也可以是.id属性(首选,因为它避免了在对象周围散布对象引用,从而避免了GC):
.id
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> ); });
ew,这比我原本打算的要长得多。
TL,DR: 切勿将数组索引(迭代索引)用作key。充其量,它模仿的是默认行为(自上而下的子级重新排序),但更多情况下,它只是将所有差异推送到最后一个子级。
编辑: @tommy建议将此链接链接到eslint- plugin-react docs,这比我上面做的要好。