【译】索引作为键是一种反模式(Index as a key is an anti-pattern)

479 阅读4分钟

写在前头

原文地址

我多次看到开发人员在呈现列表时使用项的索引作为键。

{
    todos.map((todo, index) => (
        <Todo {...todo} key={index} />
    ));
} 

它看起来很优雅,而且去掉了警告(这才是“真正的”问题,对吧?)这里的危险是什么?

它可能会破坏你的应用程序和显示错误的数据!

让我解释一下,键是React用来识别DOM元素的唯一工具。如果您将一个项目推入列表或删除中间的内容会发生什么?如果键与之前相同,React假定DOM元素表示与之前相同的组件。但这种情况已不复存在。

image.png

为了演示潜在的危险,我创建了一个简单的示例(带有源代码)

image.png

示例的截图,显示了使用索引作为键的危险。

结果是,当不传递任何东西时,React使用索引作为键,因为它是目前最好的猜测。此外,它还会警告你,这是次优的(它说的话有点令人困惑,是的)。如果你自己提供它(键),React会认为你知道你在做什么,记住这个例子,会导致不可预测的结果。

Better

每一个这样的项目都应该有一个永久的和唯一的属性。理想情况下,应该在创建项时赋值。当然,我说的是一个id。然后我们可以用下面的方法来使用它:

{
    todos.map((todo) => (
        <Todo {...todo} key={todo.id} />
    ));
}

注意:首先查看项目的现有属性。有可能他们已经有了可以用作身份证明的东西。

一种方法就是抽象地把编号往上移一步。使用全局索引(global index)可以确保任何两个项目具有不同的id。

let todoCounter = 1;
const createNewTodo = (text) => ({
    completed: false,
    id: todoCounter++,
    text
}

Much better

生产解决方案应该使用更健壮的方法来处理项的分布式创建。因此,我推荐nanoid。它快速生成短的非顺序的url友好的唯一id。代码可能如下所示:

import { nanoid } from 'nanoid';
const createNewTodo = (text) => ({
    completed: false,
    id: nanoid(),
    text
}

注意:

nanoid文档声明它不应该与React一起使用。他们提到在渲染过程中使用它,这是绝对正确的。在这里,我使用它创建一个稳定的ID。所以不要担心,我们谈论不同的事情,这个建议仍然有效。

TL;DR:为每个项目生成一个唯一的id,并在呈现列表时使用它作为键。

Update: Exception from the rule

很多人问他们是否总是要生成id。其他人提出了一些用例,其中使用索引作为键似乎是合理的。

的确,有时生成新的id是多余的,可以避免。例如,翻译许可条款或贡献者名单。

为了帮助你做出决定,我将这些例子的三个共同点放在一起:

  1. 列表和项是静态的—它们不计算也不更改;
  2. 列表中的项目没有id;
  3. 列表从不重新排序或过滤。

当满足所有这些条件时,您可以安全地将索引用作键。

Update 2: React, Preact, and *react

尽管在这篇文章中我写的是React,但问题并不仅限于它。在类似的库中,比如Preact,危险也存在。然而,效果可能是不同的。

请参阅下面的StackOverflow问题,其中最后一个元素消失了。也请注意Preact的创建者提供的答案中的解释, Jason Miller

Wrong components rendered by Preact

Update 3: nanoid

之前本指南推荐的是shortid.。这个ID生成器被弃用了,所以我用nanoid.代替了它。

Update 4: Clarify nanoid + React usage

正如一些人评论的那样,nanoid不建议直接在React的组件渲染中使用。读完这篇文章,我希望你知道为什么。我添加了一个注释,澄清本文中的建议仍然有效。

参考资料及相关文章

如果你喜欢这篇文章,请不要忘记在下方留言👏。每次鼓掌通知对我来说都是一种激励。 如果你想了解更多,我开设了一个关于JavaScript的YouTube频道,请考虑订阅。从一开始就陪伴我,帮助我变得更好。