React 列表渲染与 key
解释列表协调中 key 的身份作用,说明 index key 的风险与稳定 id 的工程实践。
#concept
#growing
#frame
#react
[!info] Related notes
- 所属:React 基础 MOC
- 路线:React 学习路线
- 相关:渲染与重渲染 · 列表性能
React 列表渲染与 key
这个概念为什么出现
库存表格增删排序后,输入框“串行”、动画错位、组件状态贴错 SKU——根因常常是 key 没有代表稳定身份。key 不是“去掉 warning 的装饰”,而是协调器识别子节点的身份证。
[!abstract] 一句话理解
key告诉 React:这一项是“同一个逻辑实体”还是“新实体”;身份错了,状态就会跟错人。
最小可运行示例
场景:可编辑的低库存列表
import { useState } from 'react'
type Row = { id: string; name: string }
export function LowStockEditor() {
const [rows, setRows] = useState<Row[]>([
{ id: 'A-01', name: '缠绕膜' },
{ id: 'A-09', name: '防潮纸板' },
])
function reverse() {
setRows((prev) => [...prev].reverse())
}
return (
<div>
<button type="button" onClick={reverse}>反转排序</button>
<ul>
{rows.map((row) => (
// 教学点:用稳定 id;若用 index,反转后输入状态会串到另一行
<li key={row.id}>
<label>
{row.id}
<input defaultValue={row.name} />
</label>
{/* 反例实验:给 map 回调加 index 参数,再改成 key={index} */}
</li>
))}
</ul>
</div>
)
}
将组件挂载到 React + TypeScript 应用,在第一行修改输入值,再点反转:编辑值应仍属于 A-01。若改用 index key,原位置的非受控输入会被另一 SKU 复用,编辑值可能错配。defaultValue 只提供初始值,本例不把编辑结果保存回 rows。
结合场景关注点
- 本地 state/非受控输入最容易暴露错误 key。
- 排序/过滤/插入中间元素时 index key 最危险。
- key 应在兄弟列表内唯一,不是全局 UUID 仪式。
核心模型
协调时 React 比较同层子节点:
- 同一父节点内,相同 type + 相同 key 允许复用组件/DOM 并保留局部状态;这不等于跳过 render
- key 或 type 变化会形成新身份,旧子树卸载,新子树挂载,局部 state 重置
- key 只在对应父节点的子列表内有意义;同 key 移到另一个父节点不能保证保留状态
边界
- 不增删、不重排、没有需要随实体保留的局部状态时,index key 风险较低;动态列表优先稳定业务 id
- key 不能神奇加速;错误 key 只会制造 bug
- 不要每次渲染生成随机 key;临时项的 id 应在创建数据时生成并保存
- key 是 React 使用的特殊信息,不作为普通 prop 传入组件;子组件需要 id 时应另外传入
- map 返回组件时把 key 放在该组件元素上;多节点分组可用
<Fragment key={id}>,简写<>...</>不能携带 key
常见误区
[!warning] 常见误区:key={Math.random()} 每次渲染产生新身份,旧局部状态丢失,并增加卸载和挂载成本。
工程实践
- 后端实体用主键;前端临时项用客户端稳定 id
- 虚拟列表场景 key 仍要稳定
- 动画库更依赖正确身份
验证记录:React 19.3.0 + jsdom 30.1.2 下,运行原组件并修改非受控输入,再反转列表;稳定 id 保留输入所属实体,index 反例将该输入复用于另一实体。此验证覆盖 DOM 身份与输入值,不覆盖动画库。
本节总结
列表的正确性先于性能:先让 key 代表身份,再谈 memo 与虚拟化。
自测题
- 为什么过滤列表时 index key 会出问题?
- key 写在
<li>还是子组件根上?
参考答案
- 过滤改变索引与实体对应关系,状态易贴错。
- 写在 map 直接返回的元素/组件/Fragment 上;仅放在子组件内部的 li 上不能标识外层组件列表。