React中为什么最好不要用数组的index作为key
当你在写或者在读别人渲染列表的React代码的时候,很容易看到使用index
作为Key
的情况,比如:
list.map((row, index)=>(
<Item key={index} />
))
看起来不错,还可以避免React
的Key
Warning
。但是,这样做却是最常见的React
错误之一,至少它很危险!尤其是在你增加
、移除
、重新排序
或者过滤
你的数组的时候。我们看一个简单的例子:
在这个例子中,我们有一个列表以及一个按钮。当点击按钮的时候,我们就会向数组list的 最前面 增加一个记录。
运行之后,我们在Item 0 对应的 input
中输入 Item 0:
但是在我们点击按钮之后,你就会发现。。。。
之前的第一个input
居然还在原来的位置,这是为什么呢?
实际上这是因为每一个Item在渲染的时候对应的key不是固定的,导致React
以为第一行的input
永远是key=0
的那个input
换句话说,一开始的时候,对于 ['Item 0']
来说,渲染Item
的时候传递的key
是 0
,于是就有了这样的关系:
- Item key={0}
- label `Item 0`
- Input value={Item[key=0].props.text}
当我们增加一个 Item 的时候,因为是在数组顶部增加的,所以就会变成: ['Item 1', 'Item 0']
对应的 Item 1
的 key
是 0
,Item 0
的 key
是 1
,关系就变成了这样:
- Item key={0}
- label `Item 1`
- Input value={Item[key=0].children.input}
- Item key={1}
- label `Item 0`
- Input value={Item[key=1].children.input}
因此React认为第一个Item
的 <input>
元素没有改变(仍然是 Item[key=0]
的 input
),于是就在第一列(此时已经是Item 1
了)显示了老的<input/>
即 Item[key=0]
的 <input>
。
为什么有的时候这样用是正常的呢?
实际上我们大多数的使用场景是在渲染一个不会改变的Array对象或者只是在数组的尾部增加新的Item。而在这些情况下,使用index
是相对安全的,因为Item的排序和key
的关系是不会发生改变的。
换句话来说,在以下的情况下是安全的:
- 数组内容不会发生改变的
- 数组不会进行filter操作的
- 数组不会重新排序的
- 数组是一个LIFO队列(后入先出),即上面说的只会在数组尾部增加内容的情况
反面教材
既然index
不能用了,那么我们可不可以用随机变量呢,比如Math.random()
// 反面教材
list.map((row, index)=>(
<Item key={Math.random()} />
))
或者其他类似的方法,如 +new Date() + Math.random()
, Symbol()
等?
这些方法都会导致每次产生的key
是不一致的,会强制让React以为自己遇到了新的内容需要重新渲染,严重影响React的性能。
我们再看一个例子,把上面的例子中的 {index}
改成 {Math.random()}
试试:
运行之后,我们添加几个Item,然后随便输入一些值:
然后点击Add,你会发现所有输入过的文本都没了?:
没错,这就是因为每次渲染的 key
都是一个新值,导致React每次都会重新创建一个新的 input
榜样教材
根据React官方提示,最好的解决方案是对每一个数据集合中的 item 使用一个唯一ID
。比如,如果你是从数据库获得的list,可以把数据库的主键/ID
返回回来作为这个唯一id;或是是自己为每一个数据项目在本地产生一个唯一且固定的ID(考虑到带来的复杂性,其实这并不是最推荐的做法)。 下面的例子使用了row.uid
,就是一个很好的例子。
list.map((row)=>(
<Item key={row.uid} />
))
至于服务端确实没办法提供uid的情况,需要在本地拿到数据的时候加上uid的方案(第三方或者自己写的UUID库生成),正如上面提及的,这种操作可能带来额外的复杂性,并不是最推荐的做法。下面是简单的例子,featchData负责获取数据,再获取数据之后,我们为每一条数据增加一个独立的uid,然后再传递给list使用:
featchData() {
// ... return data.list
let list = data.list.map(item => {
return {uid: SomeLibrary.generateUniqueID(), value: item};
});
setList(list)
}
// ...
<>
{list.map((row) => (
return <Item key={row.uid} />;
))}
</>
虽然不是最好的方案,但是还是顺便推荐一个产生唯一ID的库,以备不时之需
- Nano ID https://github.com/ai/nanoid/