React 列表渲染 key 用错的代价:重渲染、状态错位与正确选 key几乎每个 React 新手都写过list.map((item, index) Li key{index} /),控制台不报警告了,看起来一切正常。直到某天列表能删除、能排序,你发现删掉第 2 项后勾选状态跑到了别的行上——这就是 key 用错的经典事故。这篇讲清楚 key 到底是干什么的,为什么index是坑,以及没有稳定 id 时怎么办。key 到底解决什么问题React 更新时会对比新旧两棵虚拟 DOM 树。对于列表,它需要判断「这一项到底是新增、删除,还是只是移动了位置」。key 就是每个元素的身份证:key 相同 → React 认为是同一个元素,复用它的 DOM 和组件状态;key 不同 → 销毁重建。问题的核心在于:当你用 index 当 key,元素的「身份」就绑定到了它的位置,而不是它的数据。用 index 当 key 的翻车现场看一个能编辑的待办列表,每行一个非受控输入框(输入框的值由 DOM 自己维护,正好暴露状态错位):function TodoList() { const [todos, setTodos] useState([ { id: a, text: 买菜 }, { id: b, text: 写代码 }, { id: c, text: 健身 }, ]); const removeFirst () setTodos(todos.slice(1)); // 删掉第一项 return ( div button onClick{removeFirst}删除第一项/button {todos.map((todo, index) ( // ❌ 用 index 当 key div key{index} input defaultValue{todo.text} / /div ))} /div ); }操作复现:先在三个输入框里分别改成「A」「B」「C」,然后点「删除第一项」。你期望剩下「B」「C」两行,结果却是「A」「B」——最后一行「C」凭空消失了。为什么?删除后数组变成[{b},{c}],index 重新从 0 开始编号:位置 index删除前删除后0买菜(值 A)写代码1写代码(值 B)健身2健身(值 C)—React 看到key0前后都在,就复用了第一行的 DOM——包括你输入的「A」;key1也复用第二行的「B」;key2消失了,于是「C」被销毁。数据删了第一条,视觉上却像删了最后一条,输入的内容全部错位。正确写法:用数据本身的稳定 id把 key 换成数据的唯一标识,身份就绑到数据上了:{todos.map((todo) ( // ✅ 用稳定的业务 id div key{todo.id} input defaultValue{todo.text} / /div ))}现在删除第一项后,keya消失、keyb/keyc保留,React 精确地只销毁第一行,「B」「C」两行的 DOM 和输入值原样保留。这才是符合直觉的结果。没有 id 怎么办后端数据一般都有主键,直接用。真正棘手的是纯前端造出来的列表(比如一个「动态增删的表单行」),数据本身没有 id。几种做法从好到差:新增时就分配一个稳定 id,这是最推荐的。用crypto.randomUUID()或一个自增计数器:const [rows, setRows] useState([]); const addRow () setRows(prev [ ...prev, { id: crypto.randomUUID(), value: }, // 创建时定 id,此后不再变 ]);用内容做 key,仅当内容唯一且不会重复时可行(比如一组不重复的标签名)。内容会变或可能重复就别用。用 index,仅在这个列表纯静态、只读、永远不增删不排序时才可接受(比如渲染一段固定的导航菜单)。一旦列表会动,index 就是定时炸弹。关键原则:key 必须在元素的整个生命周期里保持稳定,且在同级兄弟中唯一。不要在 render 里用Math.random()当 key——那样每次渲染 key 都变,React 会把整个列表全部销毁重建,状态全丢、性能极差。一个附带的性能真相除了状态错位,index 当 key 在列表头部插入时还很慢。往数组开头插一条:所有元素的 index 都后移一位,React 逐个对比发现 key 全「变了」,于是把整个列表重建一遍。用稳定 id 的话,它只需插入一个新 DOM 节点,其余原封不动。列表越长,差距越明显。小结key 是元素的身份证,决定 React复用还是重建DOM 与组件状态。用index当 key,身份绑到了「位置」;一旦增删/排序,状态会错位到别的行。正确做法:用数据自带的稳定业务 id;纯前端列表在创建时就分配一个固定 id。key 要全生命周期稳定 同级唯一;绝不用Math.random(),那等于每帧全量重建。稳定 key 还能避免头部插入时的整表重渲染,列表越长收益越大。一句话记忆点:key 绑数据,不绑位置——只要列表会动,index 就是错的。