DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

React 排序后输入内容错行

索引key会让排序后的记录复用原位置状态。给外层列表项稳定业务ID,检查用户输入、局部状态、删除与前插后的归属。

在列表一行输入文字,随后排序、删除或前插记录,行上的名称已经更新,输入内容却像留在原来的位置。先看 map() 返回的行是否用了 key={index}:React 按这个位置键复用组件或 DOM,新的业务记录可能接管旧记录的局部状态。

只看静态文本的排序,未必能发现问题。非受控 input 中的用户输入、行组件里的展开状态或草稿,能更清楚地显示“业务项移动了,状态却跟着位置”的差异。即使用新数组更新 state,也不能修复不稳定的业务身份。

把 key 放在 map 返回的最外层

每条记录需要在创建时取得稳定 ID,并随数据保存;服务器已有业务主键时可以使用它。以下是两条本地示例记录,输入由 DOM 保存,使用稳定 key 后应随对应记录移动:

import { useState } from 'react';

export default function DraftRows() {
  const [rows, setRows] = useState([
    { id: 'a', name: 'A' },
    { id: 'b', name: 'B' },
  ]);

  return (
    <>
      <button onClick={() => setRows(previous => [...previous].reverse())}>
        反转顺序
      </button>
      <ul>
        {rows.map(row => (
          <li key={row.id}>
            <label>
              {row.name}
              <input defaultValue="" aria-label={`${row.name} 的草稿`} />
            </label>
          </li>
        ))}
      </ul>
    </>
  );
}

如果 map 直接返回 <Row>,key 放在 Row 上;写在 Row 内部的 li,不会成为外层列表的身份。每项返回多个节点时,可用带 key 的显式 Fragment,短写的 <>...</> 不能承载 key。

key 只需在同一组兄弟节点中唯一,但同一业务项在连续渲染中需要保持稳定。不要在 render 中调用 Math.random() 或每次生成 UUID;那会让 React 把每轮都当成新项,重建状态与 DOM,丢掉输入或焦点。

稳定身份不等于自动保存草稿

key 不会作为普通 prop 传入 Row。子组件需要业务 ID 时,还要显式传 id={row.id} 等数据。

上例的非受控输入保存在 DOM 中,重排时身份对应;页面刷新、整项卸载后并不保证它还存在。需要跨分页、离开页面或重新加载保存草稿时,应由业务 ID 管理草稿数据,设计对应持久化与恢复。

如果采用受控输入,仍需让 value 与业务项对应,不能把同一份位置索引草稿数组错误地拼给已排序记录。控制方式警告可另外检查uncontrolled 与 controlled 切换;它与列表 key 的身份选择是不同的问题。

用有状态的操作检查修复

先给 A、B 输入不同文本,反转顺序,再删除其中一项或在前面插入新记录,检查文本仍属于原业务项。同时覆盖行组件里的局部状态和焦点行为,不只检查名称顺序。

旧 key 已经让草稿与记录混在一起时,改 key 不会自动判断每段内容原本属于谁。涉及真实表单提交,先保存可恢复的数据并核实业务对应关系;不要把一次重新挂载、输入被清空当作正确恢复。