1. 起因
代码review 的时候经常看到 key={index} 满天飞,编译器不会报错,界面看着也没问题,直到某天一个可编辑列表出现了”删除第二行,结果第三行的输入框内容跑到第二行去了”这种诡异 bug,才想起来去查 key 到底是干嘛用的。
2. key 不是给你用来消除警告的
React 官方文档写得很清楚,key 是给 diff 算法用的,帮助 React 判断列表里哪些元素是”同一个”,从而决定是复用 DOM 节点还是重新创建。很多人(包括我自己)一开始只是为了消除控制台那条”Warning: Each child in a list should have a unique key prop”的红字,随手拿 index 当 key 用,反而埋了一个隐患。
1 | {list.map((item, index) => ( |
如果 list 只是纯展示、顺序永远不变、也不会增删,用 index 没什么问题。但只要列表会排序、插入、删除,用 index 当 key 就会出问题。
3. index 当 key 为什么会导致状态错位
1 | const [list, setList] = useState([ |
假设用 key={index} 渲染这三行输入框,用户在第二行(index=1)输入了一些内容,然后点击删除第一行。删除后数组变成 [B, C],但 React 看到的 key 序列还是 [0, 1]——它认为 key=0 和 key=1 的节点还在,只是内容变了,于是复用了原来 index=0 和 index=1 的 DOM 节点,而不是把它们删掉重建。结果就是:原本第二行(B)的输入内容,现在挂在了第一行的 DOM 上,视觉上看起来像是”内容跟错了行”。
用 item.id 当 key 就不会有这个问题,因为 id 跟着数据走,删除哪一项,React 就精确知道该卸载哪个 DOM 节点:
1 | {list.map(item => ( |
4. 用随机数当 key 是另一个极端
见过有人为了”绝对不重复”,直接在 render 里用 Math.random() 或者 crypto.randomUUID() 生成 key:
1 | {list.map(item => ( |
这样每次渲染 key 都不一样,React 会认为所有节点都是全新的,之前的全部卸载,全部重新创建。表现出来就是列表每次重渲染都会闪一下,如果 Item 内部有动画、有 useEffect 挂载逻辑,会被无意义地反复触发,性能比不写 key 还差。key 一定要来自数据本身、且在组件生命周期内保持稳定,不能是渲染时现造的。
5. 判断标准
| 场景 | 建议 |
|---|---|
| 静态列表,不会增删排序 | index 可以接受 |
| 列表有排序、插入、删除,或子项有内部状态(输入框、勾选框) | 必须用数据自带的稳定 id |
| 后端没有提供 id | 前端生成一次性 id 并存进数据里(比如插入新项时 crypto.randomUUID()),而不是每次渲染都生成 |
6. 小结
key 这个属性小到经常被当成”为了消除警告加的东西”,但它直接决定了 diff 算法怎么复用 DOM。列表一旦涉及增删排序或者子项自带状态,index 当 key 基本等于埋雷,迟早会炸。