key 是 React 识别列表元素身份的标签——有了它,React 才知道”哪个是哪个”,而不是”第几个是第几个”。
一、为什么列表需要 key
1.1 没有 key 会怎样
React 更新 UI 的方式是对比新旧虚拟 DOM,做最小化修改。这个对比过程叫 Diff,核心规则见 声明式与React的实现机制。
对于列表,Diff 遇到一个问题:怎么知道新列表里的某个元素,对应旧列表里的哪个?
// 旧渲染结果
<li>Alice</li>
<li>Bob</li>
<li>Charlie</li>
// 新渲染结果(头部插入了 Dave)
<li>Dave</li>
<li>Alice</li>
<li>Bob</li>
<li>Charlie</li>
没有 key,React 只能按位置匹配:
| 位置 | 旧 | 新 | React 的理解 |
|---|---|---|---|
| 第 1 个 | Alice | Dave | “内容从 Alice 变成了 Dave” |
| 第 2 个 | Bob | Alice | “内容从 Bob 变成了 Alice” |
| 第 3 个 | Charlie | Bob | “内容从 Charlie 变成了 Bob” |
| 第 4 个 | — | Charlie | “新增了第 4 个元素” |
结果:4 次更新。而实际只需在头部插入 1 个元素。
1.2 有了 key 之后
// 旧
<li key="a">Alice</li>
<li key="b">Bob</li>
<li key="c">Charlie</li>
// 新
<li key="d">Dave</li>
<li key="a">Alice</li>
<li key="b">Bob</li>
<li key="c">Charlie</li>
React 按 key 匹配:key=“a” 还是 Alice,key=“b” 还是 Bob——都没变,不用更新。只有 key=“d” 是新的,插入即可。1 次插入,0 次更新。
💡 生活类比:key 像座位号——有座位号,换座位能找到人;没有座位号,只能按”第几个座位”找,人一换就找错。
1.3 不加 key 的警告
列表没给 key 时,React 控制台会报警:
Warning: Each child in a list should have a unique "key" prop.
这不是建议,是必须。没有 key 时 React 退回到按位置匹配(等价于 key={index}),性能和正确性都可能出问题。
二、key 的本质
2.1 兄弟节点间的唯一标识
key 是兄弟节点间的唯一标识,帮 React 在 Diff 时做最小化更新。“兄弟节点”指同一个父节点下的同一层子节点:
<ul>
<li key="a">Alice</li> {/* key="a" 只需在 <ul> 的直接子 <li> 间唯一 */}
<li key="b">Bob</li> {/* 不需要跟其他 <ul> 下的 key 不同 */}
</ul>
2.2 key 不需要全局唯一
function Page() {
return (
<div>
<ul>
<li key="a">水果:苹果</li>
<li key="b">水果:香蕉</li>
</ul>
<ol>
<li key="a">蔬菜:白菜</li> {/* ✅ 跟上面的 "a" 不冲突 */}
<li key="b">蔬菜:萝卜</li>
</ol>
</div>
);
}
💡 key 的唯一性范围 = 同一父节点的直接子节点之间。不同父节点下的 key 互不干扰。
2.3 key 是给 React 用的,不是给你的
key 不会作为 prop 传进组件:
function Item({ id, name }) {
// 拿不到 key——key 被 React 消费了,不会出现在 props 里
}
<li key="abc">
<Item id={1} name="Alice" />
</li>;
需要组件内部用到那个值?显式传一个 prop:
<li key={item.id}>
<Item id={item.id} name={item.name} />
</li>
// ↑ React 用 ↑ 组件内部用
三、用 index 作 key 的问题
最常见的坑:
{
items.map((item, index) => <li key={index}>{item.name}</li>);
}
看起来没问题,跑起来也没报错。但列表一旦增删或重排,就会出 bug。
3.1 为什么 index 作 key 会错位
index 是位置编号,不是身份标识。列表变化后,同一个 index 指向的元素变了:
初始:index 0=Alice, 1=Bob, 2=Charlie
头部插入 Dave 后:index 0=Dave, 1=Alice, 2=Bob, 3=Charlie
React 按 key(即 index)匹配:key=0 从 Alice 变成 Dave → React 以为”Alice 的内容变了”,更新而不是移动。又变成了 4 次操作,跟没有 key 一样。
3.2 具体示例:状态错位 bug
index 作 key 最致命的问题不是性能,而是状态错位:
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "学习 React" },
{ id: 2, text: "写代码" },
{ id: 3, text: "睡觉" },
]);
return (
<div>
{/* ❌ 用 index 作 key */}
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
<button
onClick={() => {
setTodos([{ id: 4, text: "新任务" }, ...todos]);
}}
>
添加到头部
</button>
</div>
);
}
function TodoItem({ todo }) {
const [editing, setEditing] = useState(false);
// 用户正在编辑 index=1("写代码")
// 插入新项后 index=1 变成了"学习 React"
// 但 React 以为 key=1 还是同一个组件实例
// → editing 状态留在了 index=1,输入框突然变成另一项!
}
// ✅ 用 id 作 key,问题消失
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} />);
}
// key 跟着数据走,不管位置怎么变,React 都能正确追踪
3.3 什么时候用 index 没问题
纯静态展示、不会增删重排的列表:
// ✅ 不会变的常量列表,用 index 没问题
const WEEKDAYS = ["周一", "周二", "周三", "周四", "周五"];
{
WEEKDAYS.map((day, index) => <li key={index}>{day}</li>);
}
条件:列表不会增删、不会重排、列表项没有内部状态。三条全满足才安全。
💡 简单判断:列表可能变(增、删、排序、过滤),就不要用 index。
四、key 的正确选择
4.1 数据的唯一 ID
// ✅ 后端返回的数据通常有 id
{
users.map(user => <UserCard key={user.id} user={user} />);
}
// ✅ 本地创建的数据,用 uuid
const addTodo = text => {
setTodos([...todos, { id: crypto.randomUUID(), text }]);
};
4.2 组合 key
单一字段不唯一时,用多个字段组合:
// ✅ 同一用户可能在多个组,单用 userId 不唯一
{
groups.map(group =>
group.members.map(member => (
<MemberCard key={`${group.id}_${member.userId}`} member={member} />
))
);
}
4.3 选择 key 的原则
| 原则 | 说明 |
|---|---|
| 稳定 | 同一条数据,key 永远一样 |
| 唯一 | 同一兄弟组内,key 不重复 |
| 可预测 | 不要用随机值(每次渲染都变) |
| 有意义 | key 跟数据身份绑定,跟位置无关 |
五、列表渲染的完整模式
5.1 Array.map() 返回 JSX 数组
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
<span>{user.name}</span>
<span>{user.email}</span>
</li>
))}
</ul>
);
}
为什么是 map 而不是 forEach? map 返回新数组,forEach 不返回值。JSX 里需要表达式,map 的返回值正好是 JSX 数组。
5.2 Fragment 作为列表容器
列表项不需要额外 DOM 包裹时,用 Fragment:
function DescriptionList({ items }) {
return (
<dl>
{items.map(item => (
// ✅ key 放在 Fragment 上
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
// ❌ 短语法 Fragment 不能传 key
<></>
// ✅ 需要传 key 时,必须用完整写法
<React.Fragment key={item.id}></React.Fragment>
5.3 列表为空的处理
function UserList({ users }) {
if (users.length === 0) {
return <p className="empty-hint">暂无用户数据</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
六、key 与组件实例的关系
6.1 key 变了 = 销毁旧实例 + 创建新实例
key 变了,React 会销毁旧组件,创建新组件。 这是最容易被忽视的特性:
function Editor({ userId }) {
const [content, setContent] = useState("");
// ❌ userId 变了,但组件实例没变,内部状态会残留
return (
<textarea value={content} onChange={e => setContent(e.target.value)} />
);
}
// ✅ key 变了 → 组件重新挂载 → 状态全部重置
<Editor key={userId} userId={userId} />;
6.2 利用 key 强制重新挂载
表单重置——提交后清空所有输入:
function ContactForm() {
const [formKey, setFormKey] = useState(0);
const handleSubmit = () => {
submitForm();
setFormKey(k => k + 1); // key 变了 → 表单组件重新挂载 → 输入清空
};
return (
<div>
<Form key={formKey} />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
切换标签页时重置状态:
// key 变了 → 旧面板销毁,新面板创建,状态不残留
<TabPanel key={activeTab} tabId={activeTab} />
💡 记住:key 变 → 销毁重建;key 不变 → 复用更新。这个规则是 key 一切行为的根基。
七、常见陷阱
7.1 用 Math.random() 作 key
// ❌ 每次渲染 key 都不一样
{
items.map(item => <li key={Math.random()}>{item.name}</li>);
}
比没有 key 还糟糕——每次渲染所有 key 都变,React 销毁并重建所有列表项。状态丢失,DOM 操作暴增。
💡 key 的核心价值是稳定。
Math.random()完全违背了这个原则。
7.2 用 index + 动态列表
// ❌ 列表会增删,却用 index 作 key
{
todos.map((todo, index) => <TodoItem key={index} todo={todo} />);
}
增删时状态错位,详见第三章。修复:给每条数据加 id。
7.3 key 相同但内容不同
// ❌ 所有项用了同一个 key
{
items.map(item => <li key="item">{item.name}</li>);
}
// ❌ 更隐蔽:数据源 id 重复
const data = [
{ id: 1, name: "Alice" },
{ id: 1, name: "Alice (duplicate)" }, // id 重复!
];
{
data.map(item => (
<li key={item.id}>{item.name}</li> // 两个 key=1
));
}
React 会警告 Encountered two children with the same key。修复:确保 key 唯一,数据有重复时用组合 key:
// ✅ 加 index 区分重复 id
{
data.map((item, index) => <li key={`${item.id}_${index}`}>{item.name}</li>);
}
7.4 陷阱速查表
| 陷阱 | 后果 | 修复 |
|---|---|---|
key={Math.random()} | 每次渲染全部重建 | 用数据的稳定 id |
key={index} + 动态列表 | 状态错位 | 用数据的稳定 id |
| 所有项 key 相同 | 只渲染最后一项 | 确保 key 唯一 |
| 数据源 id 重复 | 警告 + 渲染异常 | 用组合 key |
八、一句话总结
key 是列表元素的身份证号——稳定、唯一、跟数据走。用 id 不用 index,key 变了就重建,不变就复用。
相关:
- 声明式与React的实现机制 — Diff 算法的上层逻辑
- React渲染机制深入-Fiber与调度 — Fiber 架构如何处理列表更新
- useState完全指南 — key 变化导致状态重置的底层原因
- React组件与函数本质 — 组件实例与 key 的关系