React Key
1. Key 的引入
想象一下,我们渲染了一个项目列表。当这个列表的数据更新时,React 需要弄清楚 UI 该如何变化。它面临两个选择:
- 销毁整个列表,然后从头开始重新创建:这种方式简单粗暴,但效率极低,会导致所有相关的 DOM 节点和组件实例被重新创建。
- 智能地更新:计算出哪些项是新增的、哪些被删除了、哪些只是移动了位置,然后只对发生变化的项进行最小化的操作。
React 选择了第二种方式,但这需要一种机制来在两次渲染之间追踪每一个列表项的“身份”。key
就是 React 用来识别每个列表项的唯一且稳定的身份标识。当列表更新时,React 会查看新旧两个列表:
- 如果一个带有特定
key
的项在旧列表中存在,但在新列表中消失了,React 就会销毁对应的组件。 - 如果一个
key
在新列表中是全新的,React 就会创建一个新的组件。 - 如果两个列表中都存在相同的
key
,React 会认为这是同一个组件,并对它进行更新(如果其内容有变化)或移动(如果其位置有变化)。
2. Key 使用准则
基本语法和位置
key
应该被放置在 map()
方法内部返回的最外层元素上。
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
// key 应该放在这里,而不是放在子组件的内部元素上
<li key={todo.id}>
{todo.task}
</li>
))}
</ul>
);
}
key
是一个只给 React 使用的内部提示,它不会作为 prop 传递给你的组件。如果我们需要在组件内部访问这个 ID,应该用一个不同的 prop 名称(如id
)再次传递它。
<TodoItem key={todo.id} id={todo.id} task={todo.task} />
Key 选择标准
一个理想的 key
应该具备两个核心特征:
- 唯一性 (Unique): 在当前列表的所有兄弟节点中,
key
必须是唯一的。不同的列表可以有相同的key
。 - 稳定性 (Stable): 对于一个特定的列表项,它的
key
在多次渲染之间应该保持不变。它不应该在渲染过程中被重新生成。
一般我们使用数据中自带的、能够唯一标识该条目的字符串或数字作为 key
。通常这是来自数据库的主键,如 user.id
或 product.uuid
。
const todos = [
{ task: "mow the yard", id: "a1b2c3d4" },
{ task: "Work on Odin Projects", id: "e5f6g7h8" },
{ task: "feed the cat", id: "i9j0k1l2" },
];
3. Key 的常见错误用法
使用数组索引作为 Key
这是最常见也是最危险的错误。key={index}
只有在一种情况下是安全的:列表是完全静态的,永远不会被重新排序或过滤。
key
的目的是帮助 React 追踪元素的身份,而索引追踪的是元素在数组中的位置。当列表发生变化时,位置和身份就会脱节,这会导致很严重的渲染问题。
在渲染时动态生成 Key
永远不要在渲染过程中即时生成 key
。
// 错误!
{items.map(item => (
<li key={Math.random()}>{item.text}</li>
))}
// 同样错误!
{items.map(item => (
<li key={crypto.randomUUID()}>{item.text}</li>
))}
这样做会在每次渲染时都为每个列表项生成一个全新的 key
。这等于告诉 React:“上一次渲染的所有东西都消失了,现在这是一批全新的东西。” React 别无选择,只能销毁所有旧的 DOM 节点和组件实例,然后创建一套全新的。这完全违背了使用 key
的初衷,会导致极差的性能,并且组件的任何内部状态(如输入框内容、动画状态)都会在每次渲染时丢失。
4. 使用 Key 强制重置组件
key
不仅仅用于列表。你可以将 key
放在任何组件上,以实现一种强大的功能:当 key
改变时,强制 React 销毁旧的组件实例并创建一个全新的实例。这在我们需要重置一个有内部状态的组件时非常有用。
下面是一个例子:假设我们有一个用户个人资料页面组件 <UserProfile />
,它在自己的 state
中管理着用户数据。当用户从一个个人资料页(比如 /users/1
)导航到另一个(/users/2
)时,我们希望整个组件被重置,而不是尝试在现有组件上更新数据。
function App({ userId }) {
// 当 userId 从 1 变为 2 时,React 会销毁旧的 UserProfile 组件
// 并创建一个全新的 UserProfile 组件。组件内部的 state 会被完全重置。
return <UserProfile key={userId} userId={userId} />;
}
通过将 userId
同时用作 key
和 prop
,我们确保了每次用户切换时,都能得到一个干净、无副作用的新组件实例。