Preact 渲染的错误组件
Wrong components rendered by 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
语句,所以看起来它们不是我的问题)。
这是一个经典的问题,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
) 会被丢弃,因为它是后续渲染中缺少的一项。
修复
因此,要修复您的示例 - 您应该使用标识 item 而不是其 offset 的内容作为您的密钥。这可以是项目本身(任何值都可以作为键),或 .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 recommended this excellent link to the eslint-plugin-react docs,这比我上面做的解释得更好。
我正在使用 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
语句,所以看起来它们不是我的问题)。
这是一个经典的问题,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
) 会被丢弃,因为它是后续渲染中缺少的一项。
修复
因此,要修复您的示例 - 您应该使用标识 item 而不是其 offset 的内容作为您的密钥。这可以是项目本身(任何值都可以作为键),或 .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 recommended this excellent link to the eslint-plugin-react docs,这比我上面做的解释得更好。