深度解析使用索引作为 key 的负面影响

456 阅读4分钟

声明:本文非原创,是译文,如有疑问,请参阅原始英文文档

原文:Index as a key is an anti-pattern

我曾经多次看到开发人员在渲染列表时使用项目的索引作为其键。

list.map((item, index) => (  
    <Item value={item} key={index} />  
)); 

它看起来很优雅,而且确实消除了警告(这才是“真正”的问题,对吧?)。那么这里存在什么隐患呢?

这可能会破坏应用程序并显示错误的数据!

让我解释一下,key 是 React 用来识别 DOM 元素的唯一标识符。如果你将一个新选项插入到到列表中或在中间删除某些选项会发生什么?如果 key 与之前相同,则 React 假定 DOM 元素表示与之前相同的组件。但这样它就不对了

image.png

警告:当我使用 map 函数的 index 参数作为映射 React 组件的 key 时,我遇到了一个严重的 UI 问题。

state 是对象列表。

容器组件对 state 中的对象列表进行排序,并将其映射到较小的 UI 组件上。

它完全按预期工作,直到我向 state 添加了一个新项并再次进行排序/映射。

此时,React 混淆了哪些属性属于哪个映射组件,并让我质疑自己所知道关于 React 的一切。

一旦我改用 uuid 作为映射组件的键,一切都重新开始正常运行。

为了展示潜在的问题,我创建了一个简单的例子(包含源代码)。

大家可以看一下这个 gif 图,在输入姓名和性别之后,试图在列表头部添加一些新选项。然而,预期中各个字段和对应值应该呈现正确的对应关系,实际上,姓名和性别的值始终维持在前两项,这种情况明显是不正确的。

react-key索引错误示例.gif

事实证明,当没有传递 key 时,React 使用索引作为 key,因为这是目前最好的猜测。 此外,它会警告你这是次优选择(它用一个有点令人困惑的词说,是的)。 如果你自己提供 key,则 React 会认为你知道自己在做什么——记住上面的例子——可能会导致不可预测的结果。

相对稳妥的解决方案

每个这样的项目都应该有一个永久且唯一的属性。理想情况下,它应该在创建选项时分配。当然,我说的是ID。然后我们可以按照以下方式使用它:

list.map(item => (
    <Todo {...item} key={item.id} />  
))

注意:首先查看选项的现有属性中是否已经存在可以用作 id 的东西,如果有,优先使用

另一种方法是使用类似数据库主键 ID 自动递增的方式,先初始化一个全局的索引值,随后每生成一个选项,该值就自动往上加一:

let counter = 1
const createItem = text => ({
    completed: false,
    id: counter++,
    text
}

更好的解决方案

在生产环境中,应该采用更健壮的方法来处理分布式创建项。为此,我推荐nanoid。它可以快速生成简短、非连续、URL友好的唯一ID。代码可能如下所示:

import { nanoid } from 'nanoid'

const createItem = text => ({
  completed: false,
  id: nanoid(),
  text
}

注意:nanoid 声明它不应该和 React 一起使用。他们指的是在渲染期间使用它(key={nanoid()}),这是绝对正确的。在这里,我使用它来创建一个稳定的ID。所以不用担心,我们谈论的是不同的事情,这个建议仍然有效。

规则的例外

许多人问是否总是必须生成ID。其他人提出了使用索引作为键时合理的案例。

确实有时候生成新的ID是多余的,可以避免。例如许可条款或贡献者列表的翻译。

为了帮助你做决定,我列出了这些案例共同具有的三个条件:

  1. 列表和项目是静态的-它们不会被计算也不会更改;
  2. 列表中的项目没有ID;
  3. 该列表永远不会被重新排序或过滤。

参考文档和相关文章

如果您喜欢这篇文章,请别忘了在下面点赞👏。每一个点赞的通知都是对我极大的鼓励。

如果您想要学习更多,我开设了一个关于 JavaScript 的 YouTube 频道,请考虑订阅。从一开始就参与其中,帮助我变得更好。

image.png