[英]Wrong components rendered by 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)});
}
}
我究竟做错了什么? 我需要以某种方式主动重新渲染吗? 这是一个n + 1案件吗?
澄清 :我的问题不在于国家的同步性。 在上面的列表中,如果我选择删除'c',状态会正确更新为['a','b','d','e']
,但呈现的组件是['a','b','c','d']
。 每次调用removePackage
,都会从数组中删除正确的一个,显示正确的状态,但会呈现错误的列表。 (我删除了console.log
语句,所以看起来它们不是我的问题)。
这是Preact的文档完全没有提供的经典问题,所以我想亲自为此道歉! 如果有兴趣,我们一直在寻找帮助撰写更好的文档。
这里发生的是你使用数组的索引作为键(在渲染中的地图中)。 这实际上只是模拟VDOM diff默认工作的方式 - 键总是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库只知道你在每个渲染上给出它的新结构(而不是如何从旧结构改为新结构),所以键做的基本上是告诉它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文档的优秀链接,它比我上面做的更好地解释它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.