React中为什么最好不要用数组的index作为key

avatar
Mofei Zhu

当你在写或者在读别人渲染列表的React代码的时候,很容易看到使用index作为Key的情况,比如:

list.map((row, index)=>(
    <Item key={index} />
))

看起来不错,还可以避免ReactKey Warning。但是,这样做却是最常见的React错误之一,至少它很危险!尤其是在你增加移除重新排序或者过滤你的数组的时候。我们看一个简单的例子:

在这个例子中,我们有一个列表以及一个按钮。当点击按钮的时候,我们就会向数组list的 最前面 增加一个记录。

运行之后,我们在Item 0 对应的 input 中输入 Item 0:

但是在我们点击按钮之后,你就会发现。。。。

之前的第一个input居然还在原来的位置,这是为什么呢?

实际上这是因为每一个Item在渲染的时候对应的key不是固定的,导致React以为第一行的input永远是key=0的那个input

换句话说,一开始的时候,对于 ['Item 0'] 来说,渲染Item的时候传递的key0,于是就有了这样的关系:

- Item key={0}
    - label `Item 0`
    - Input value={Item[key=0].props.text}

当我们增加一个 Item 的时候,因为是在数组顶部增加的,所以就会变成: ['Item 1', 'Item 0'] 对应的 Item 1key0Item 0key1,关系就变成了这样:

- 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/

参考资料