
一句话:
key是 React 判断「新旧列表中的元素是否是同一个」的身份标识——它决定组件是「复用」还是「重建」;用 index 或随机值会在列表变动时造成状态错位或性能灾难。
一、key 的语义
// 没有 key 时,React 只能用「位置(index)」来推断身份
<ul>
{items.map((item) => <li>{item.text}</li>)} {/* ⚠️ React 会警告 */}
</ul>
// 有 key 时,React 用 key 来判断「是否同一个」
<ul>
{items.map((item) => <li key={item.id}>{item.text}</li>)}
</ul>
「同一个元素」意味着什么?
✅ 复用:
· 保留原有的 DOM 节点(不重新创建)
· 保留组件内部状态(useState / useRef / DOM 状态如输入框光标)
· 保留 DOM 上的副作用(如 CSS 过渡、媒体播放位置)
· 只更新变化的属性
❌ 重建(不复用):
· 卸载旧组件(触发清理函数、ref 置 null)
· 创建新 DOM 节点与组件实例
· 状态全部重置(输入内容、展开态、动画进度)
一句话理解:
「key 相同 = 我认为它是同一个东西,请尽量复用」
「key 不同 = 这是另一个东西,请重新造一个」
二、key 不匹配时会发生什么
// React 判断逻辑(简化)
for (每个新元素) {
if (可以在旧列表中找到 key 相同的) {
复用那个 fiber(更新 props)
} else {
创建新 fiber(Placement)
}
}
for (旧列表中「没被复用」的) {
删除(ChildDeletion)→ 卸载组件、移除 DOM
}
| 新旧对比 | React 的判定 |
|---|---|
| key 相同 + type 相同 | 复用(更新 props) |
| key 相同 + type 不同 | 删除旧的、创建新的(不复用) |
| key 不同 | 删除旧的、创建新的 |
| 新多了 | 创建(Placement) |
| 旧多了 | 删除(ChildDeletion) |
三、用 index 作 key 的三个 bug 场景
场景一:在头部插入
// ❌ 用 index 作 key
function List() {
const [items, setItems] = useState(['A', 'B', 'C']);
return (
<>
<button onClick={() => setItems(['NEW', ...items])}>头部插入</button>
<ul>
{items.map((text, i) => (
<li key={i}>
{text}
<input placeholder="备注" /> {/* ⚠️ 有内部状态的元素 */}
</li>
))}
</ul>
</>
);
}
发生了什么?
初始:[A, B, C] → key = [0, 1, 2]
插入后:[NEW, A, B, C] → key = [0, 1, 2, 3]
React 的判定(只看 key):
key 0: 之前是 A,现在是 NEW → 「同一个」(复用!)→ 只改文本,DOM 和 input 状态保留
key 1: 之前是 B,现在是 A → 「同一个」→ 复用
key 2: 之前是 C,现在是 B → 「同一个」→ 复用
key 3: 新增 → 创建
结果:文本看起来是对的(因为文本是「渲染出来的」),
但**输入框里的内容全部错位了**——
原本在 A 那一行的备注,现在出现在 NEW 那一行。
场景二:删除中间项
// 初始:[A, B, C],在 B 的输入框里输入了 "hello"
// 删除 B 后:[A, C] → key = [0, 1]
// React 的判定:
// key 0: A → A 复用(输入框内容保留:空)
// key 1: B → C 复用(⚠️ 输入框里还留着 "hello")
// key 2: 删除
// 结果:C 那一行的输入框里显示着 "hello"(本该属于已删除的 B)
场景三:排序 + 带状态的组件
// 一个可以排序的表格,每行有「展开详情」的状态
{rows.map((row, i) => (
<Row key={i} row={row} /> // ❌ 排序后展开状态跟着「位置」而不是「行」
))}
// 现象:按价格排序后,展开的那一行变成了另一条数据
// 原因:key 是 index,排序只改变了「数据在数组中的位置」,
// 而 React 认为「key 0 还是 key 0」→ 复用 DOM 与状态 → 状态错位
| 操作 | 用 index 的后果 |
|---|---|
| 列表只读、永不重排 | ✅ 没问题(可以接受) |
| 头部/中间插入 | 后续项的 DOM 与状态错位 |
| 删除 | 被删项的状态「残留」到下一项 |
| 排序 | 状态跟着位置走,与数据不匹配 |
| 有受控组件的行 | 输入内容错乱(最明显的表现) |
| 有动画的行 | 动画错位 |
// ✅ 用稳定唯一 ID
{items.map((item) => (
<li key={item.id}>
{item.text}
<input placeholder="备注" />
</li>
))}
// 现在:key 跟着数据走 → 插入/删除/排序时,「同一个数据」始终是同一个组件实例
四、没有稳定 ID 时怎么办
// ① 业务数据里加 ID(最优解)
// 数据入库/接口返回时就带上 id(数据库主键、UUID、雪花 ID)
// ② 在前端「一次性生成」并保存到数据里(注意不要每次渲染都生成)
function useItems(raw: string[]) {
const [items, setItems] = useState(() =>
raw.map((text) => ({ id: crypto.randomUUID(), text })) // ⭐ 初始化时生成一次
);
return items;
}
// ⚠️ 不要写成 {raw.map((text) => ({ id: crypto.randomUUID(), text }))} 直接放在渲染里
// → 每次渲染都是新 id → 每次都重建整列
// ③ 用「内容 + 类型」拼一个相对稳定的 key(有风险)
key={`${item.type}-${item.title}`}
// ⚠️ 只有在「内容唯一且不变」时才可行
// ④ 列表确实只读且永不重排 → 可以用 index(但要写注释说明原因)
{staticList.map((item, i) => <Row key={i} item={item} />)} // 可接受
// ② 的一个完整例子:需要「本地生成 id」的场景
function useLocalTodoList(initialTexts: string[]) {
const [todos, setTodos] = useState<Todo[]>(() =>
initialTexts.map((text) => ({ id: nanoid(), text, done: false }))
);
// ↑ 惰性初始化:只在首次渲染执行一次
const addTodo = useCallback((text: string) => {
setTodos((prev) => [...prev, { id: nanoid(), text, done: false }]);
}, []);
return { todos, addTodo };
}
五、key 的常见误用
// ① key 加在「map 返回元素的内层」
{items.map((item) => (
<div> {/* 没有 key */}
<Row key={item.id} item={item} /> {/* ⚠️ 位置错误:外层没有 key */}
</div>
))}
// ✅ 正确
{items.map((item) => (
<div key={item.id}>
<Row item={item} />
</div>
))}
// ② 用随机值当 key(每次渲染都重建整列 → 性能灾难 + 状态全丢)
{items.map((item) => <Row key={Math.random()} item={item} />)} // ❌
{items.map((item) => <Row key={crypto.randomUUID()} item={item} />)} // ❌ 同上
// ③ 用「对象」当 key(每次渲染都是新引用 → 同样全量重建)
{items.map((item) => <Row key={{ id: item.id }} item={item} />)} // ❌
// ④ 在 Fragment 简写上写 key(语法不支持)
{items.map((item) => <key={item.id}>...</>)} // ❌ 语法错误
// ✅ 必须用完整写法
{items.map((item) => <Fragment key={item.id}>...</Fragment>)}
// ⑤ 认为 key 会「传给子组件的 props」
function Row({ key, item }) { // ❌ key 不会出现在 props 里
return <div>{key}</div>;
}
// ✅ 需要传递时用别的 prop 名
<Row key={item.id} id={item.id} item={item} />
// ⑥ 用 key 让组件「每次都重新挂载」(有时是故意的,但要清楚代价)
<Component key={Date.now()} /> // ⚠️ 每秒重建
六、key 的「非列表」用途(有意为之)
// ① 用 key 重置组件状态(这是官方推荐的模式)
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState('');
// ❌ 不用 key:切换 userId 时草稿不会清空
// ✅ 用 key:userId 变化时组件重建 → 状态自然重置
return <ProfileForm userId={userId} />;
}
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />; // ⭐ 切换用户 = 换一个实例
}
「用 key 重置状态」比「用 useEffect 清空状态」好在哪里?
❌ useEffect 方案:
useEffect(() => { setComment(''); setDraft(''); setTab('info'); }, [userId]);
· 需要列出所有要重置的状态(容易漏)
· 多一次渲染
· 存在「短暂的旧数据 + 新 userId」的中间态
✅ key 方案:
<Profile key={userId} />
· 一次写清(整个组件重建)
· 没有中间态
· 语义正确(「不同用户 = 不同实例」)
// ② 用 key 强制「重新触发动画」
function FadeIn({ content, animationKey }: { content: string; animationKey: number }) {
return <div key={animationKey} className="fade-in">{content}</div>;
}
// 每次 animationKey 变化 → 元素重建 → CSS 动画重新播放
// ③ 用 key 表达「这两个是不同的东西」(避免误复用)
{isLoginMode ? <LoginForm key="login" /> : <RegisterForm key="register" />}
// ⚠️ 其实这里 type 已经不同了,key 不是必需的;但在「同一个组件、不同语义」时有用
{tab === 'a' ? <Editor key="a" docId="a" /> : <Editor key="b" docId="b" />}
// ✅ 同一个 Editor 组件,但代表不同文档 → 用 key 明确区分(避免状态串味)
七、常见坑
// ① 用 index
{list.map((x, i) => <Row key={i} />)} // ⚠️ 列表会变动时出 bug
// ② 用随机值 / 时间戳 / 对象
key={Math.random()} / key={Date.now()} / key={{ id }} // ❌ 全量重建
// ③ key 加错位置(应在 map 返回的最外层元素上)
// ④ key 重复(同一层出现相同的 key → 复用错误 + React 警告)
{items.map((x) => <Row key="same" />)} // ❌ 所有项 key 相同
// ⑤ 认为「key 不同 = 重新渲染」(其实是「卸载 + 挂载」,代价更大)
// ⑥ 把 key 当 props 用(拿不到)
// ⑦ 在嵌套列表里重复使用同一套 key(其实只要求「同层唯一」,但容易混淆)
<ul>
{groups.map((g) => (
<li key={g.id}>
{g.items.map((it) => <span key={it.id} />)} {/* ✅ 同层唯一即可 */}
</li>
))}
</ul>
// ⑧ 用 key 解决「性能问题」(反过来可能更慢)
// ⑨ 在「受控输入」的列表里用 index → 输入内容错乱(最典型的表现)
// ⑩ 认为「有 id 就一定稳定」(id 可能重复或变化,要确认来源)
// ⑪ 在 SSR 与客户端用不同的 key 生成方式(导致 hydration mismatch)
// ⑫ 忘记「key 只在其所在的那一层内唯一」,跨层重复没有关系
八、面试延伸
- 「
key有什么用?」
key是 React 用来判断「新旧列表中的元素是否是同一个」的身份标识。key 相同 → React 认为「这是同一个元素」→ 复用现有的 fiber、DOM 与组件状态,只更新变化的属性;key 不同 → 认为「换了一个东西」→ 卸载旧的、创建新的(状态重置)。所以key直接决定「复用还是重建」,影响性能与状态正确性。 -
「为什么不能用 index 作 key?」
因为index表示的是「位置」而不是「身份」。当列表插入、删除或排序时,同一个数据项的 index 会变化,而 React 会「按 key 复用」——于是把「原本属于 A 的 DOM 与状态」复用给了 B,导致:① 有状态组件的内容错位(输入框里的文字跑到另一行);② 状态残留(删除后动画/展开态跑到别的行);③ 受控组件行为异常。只有在「列表只读且永不重排」时才可以用 index。 -
「用随机值作 key 会怎样?」
会导致每次渲染都重建整个列表——因为每次生成的 key 都不同,React 认为「所有这些元素都是新的」→ 卸载全部旧节点、创建全部新节点。后果是:① 性能灾难(大量 DOM 创建/销毁);② 状态全部丢失(输入内容、展开态、动画、媒体播放位置);③ 输入框会每次都失焦。这是最严重的一类 key 错误。 -
「
key为什么必须放在map返回的最外层元素上?」
因为 React 是在比较「同层的子元素列表」时使用 key 的——它需要给「列表中的每一项」一个身份。如果你把 key 放在内层元素上,那么「外层的<div>」就没有 key(React 只能按位置匹配),而内层的 key 只在「这个 div 内部」有意义,无法表达「这一整项是谁」。所以 key 必须放在 map 回调返回的那个最外层元素上。 -
「
key会传给子组件吗?」
不会。key是 React 的保留字段,它不会被放进props。所以function Row({ key }) {}拿到的是undefined。如果子组件确实需要这个标识,要额外传一个 prop:<Row key={item.id} id={item.id} />(注意「同名字段」也可以,因为key和props.id是两回事)。 -
「可以用
key来重置组件状态吗?」
可以,而且是官方推荐的模式。当「同一个组件在不同 props 下应该是不同实例」时(如userId变化),用<Profile key={userId} />就能让 React 在 key 变化时卸载重建,从而自然地重置所有内部状态。这比「在useEffect里手动清空每一个 state」更简洁、更不易漏、也没有「短暂中间态」的问题。 -
「为什么两个元素类型不同时,key 相同也救不了?」
因为 React 的判断顺序是「先看 key,再看 type」——即使 key 相同,如果type不同(如<div>变成<section>,或两个不同的组件函数),React 仍会认为「这是不同的元素」→ 卸载旧的、创建新的。所以「跨类型复用」在 React 里是不存在的;想避免重建就必须保持同一层级的 type 稳定。 -
「key 需要全局唯一吗?」
不需要,只在「同层(同一个父元素的子元素列表)内」唯一即可。不同层级、不同列表之间可以使用相同的 key(比如两个独立的列表都有key="1")。但如果同一个列表里出现重复 key,React 会警告并且复用行为会出错(后出现的元素会覆盖前面的身份),所以要保证「同一层内不重复」。
一句话速记
key是「身份标识」——相同 key 就复用(保留 DOM 与状态),不同就重建(状态重置);用 index 会在插入/删除/排序时让状态错位(最典型的症状是「输入框内容跑到别的行」)、用随机值/对象会让整列每次重建(性能灾难 + 状态全丢);key 必须放在map返回的最外层元素、只在同层唯一、不会出现在 props 里;没有稳定 ID 就「数据里加 ID」或「惰性初始化时生成一次」(别写在渲染里);key还可以用来有意重置组件(<Form key={userId} />,比useEffect清空状态更优)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!