React 中的 key 有什么作用?

一、是什么

key 是 React 用于识别列表中每个元素身份的特殊属性。它帮助 React 在 Reconciliation(协调)过程中高效地判断哪些元素发生了变化、被添加或被移除。

key 不是传递给组件的 prop(子组件无法通过 props.key 获取),它仅供 React 内部使用。

function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

二、key 在 Reconciliation 中的作用

React 采用 Diff 算法比较前后两棵虚拟 DOM 树来计算最小更新操作。对于列表元素的 Diff,React 依赖 key 来建立新旧元素之间的对应关系。

没有 key 的情况

如果没有提供 key,React 默认按照索引逐个比较。当列表顺序改变时,这种方式会导致大量不必要的 DOM 更新。

假设列表从 [A, B, C] 变为 [C, A, B]

无 key(按索引比较):
位置 0:A → C(更新内容)
位置 1:B → A(更新内容)
位置 2:C → B(更新内容)
→ 3 次 DOM 更新

有 key(按 key 匹配):
key="A":位置 0 → 位置 1(移动)
key="B":位置 1 → 位置 2(移动)
key="C":位置 2 → 位置 0(移动)
→ DOM 移动操作,无内容更新

Diff 过程图解

React 对列表进行 Diff 时的步骤:

  1. 用旧列表的 key 建立 Map:{ key → Fiber }
  2. 遍历新列表,用每个元素的 key 在 Map 中查找
  3. 找到了:复用旧 Fiber,标记为移动或更新
  4. 没找到:创建新 Fiber
  5. Map 中剩余的 key:标记为删除
// 第一次渲染
const listV1 = [
  { id: 'a', text: '学习 React' },
  { id: 'b', text: '学习 TypeScript' },
  { id: 'c', text: '学习 Next.js' },
];

// 第二次渲染:在头部插入一条
const listV2 = [
  { id: 'd', text: '学习 Rust' },      // 新增
  { id: 'a', text: '学习 React' },      // 复用
  { id: 'b', text: '学习 TypeScript' }, // 复用
  { id: 'c', text: '学习 Next.js' },   // 复用
];

有正确的 key 时,React 知道 abc 三个元素没有变化,只需要创建 d 并将其插入头部。

三、为什么不应该使用 index 作为 key

使用数组索引作为 key 在列表会重排、插入或删除时会导致严重问题。

问题一:状态错乱

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: '任务 A' },
    { id: 2, text: '任务 B' },
    { id: 3, text: '任务 C' },
  ]);

  const addToTop = () => {
    setTodos([{ id: Date.now(), text: '新任务' }, ...todos]);
  };

  return (
    <div>
      <button onClick={addToTop}>在顶部添加</button>
      <ul>
        {todos.map((todo, index) => (
          // ❌ 使用 index 作为 key
          <li key={index}>
            <TodoItem text={todo.text} />
          </li>
        ))}
      </ul>
    </div>
  );
}

function TodoItem({ text }: { text: string }) {
  const [checked, setChecked] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={checked}
        onChange={e => setChecked(e.target.checked)}
      />
      {text}
    </label>
  );
}

假设用户勾选了"任务 A"(index=0),然后点击"在顶部添加"。新列表中 index=0 变成了"新任务",但 React 认为 key=0 的元素没变(还是同一个位置),于是复用了之前的组件实例——导致"新任务"显示为已勾选,而"任务 A"变成未勾选。

问题二:性能退化

// 在列表头部插入元素
// 旧列表 key: [0, 1, 2]
// 新列表 key: [0, 1, 2, 3]
//
// 使用 index 作为 key:
// key=0: 旧元素 A → 新元素 D(更新内容)
// key=1: 旧元素 B → 新元素 A(更新内容)
// key=2: 旧元素 C → 新元素 B(更新内容)
// key=3: 不存在 → 新元素 C(创建)
// → 3 次不必要的更新 + 1 次创建

// 使用 id 作为 key:
// key='d': 不存在 → 新元素 D(创建)
// key='a': 存在,内容相同(跳过)
// key='b': 存在,内容相同(跳过)
// key='c': 存在,内容相同(跳过)
// → 仅 1 次创建

何时可以使用 index

以下条件同时满足时,使用 index 作为 key 是安全的:

  1. 列表是静态的,不会重排、插入或删除
  2. 列表项没有自己的内部状态
  3. 列表项没有使用非受控的输入元素
// ✅ 静态导航菜单,使用 index 是安全的
const menuItems = ['首页', '关于', '联系'];

function Nav() {
  return (
    <nav>
      {menuItems.map((item, index) => (
        <a key={index} href={`/${item}`}>{item}</a>
      ))}
    </nav>
  );
}

四、key 的最佳实践

使用稳定、唯一的标识符

// ✅ 使用数据库 ID
{users.map(user => <UserCard key={user.id} user={user} />)}

// ✅ 使用 UUID 或其他唯一标识
{messages.map(msg => <Message key={msg.uuid} message={msg} />)}

// ✅ 组合多个字段形成唯一 key
{items.map(item => (
  <Row key={`${item.category}-${item.name}`} item={item} />
))}

避免使用随机值

// ❌ 每次渲染都生成新的 key,导致所有子组件被销毁重建
{items.map(item => (
  <div key={Math.random()}>{item.text}</div>
))}

// ❌ crypto.randomUUID() 同理
{items.map(item => (
  <div key={crypto.randomUUID()}>{item.text}</div>
))}

五、利用 key 重置组件状态

key 不仅用于列表,还可以用来强制重置组件的内部状态。当 key 变化时,React 会销毁旧组件并创建新组件。

切换用户时重置表单

function UserEditor({ userId }: { userId: string }) {
  return (
    <div>
      <h2>编辑用户</h2>
      {/* key 变化时,EditForm 会被完全重置 */}
      <EditForm key={userId} userId={userId} />
    </div>
  );
}

function EditForm({ userId }: { userId: string }) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  useEffect(() => {
    fetchUser(userId).then(user => {
      setName(user.name);
      setEmail(user.email);
    });
  }, [userId]);

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

重置动画状态

function AnimatedNotification({ message, id }: { message: string; id: number }) {
  return (
    <div key={id} className="notification fade-in">
      {message}
    </div>
  );
}

每次 id 变化时,整个 DOM 元素会被重新创建,CSS 动画会重新播放。

强制重新挂载第三方组件

function ChartWrapper({ config }: { config: ChartConfig }) {
  const configKey = JSON.stringify(config);

  return (
    <div>
      {/* 配置结构性变化时,重新初始化图表 */}
      <ThirdPartyChart key={configKey} config={config} />
    </div>
  );
}

六、key 与 Fragment

当需要在列表中返回多个元素时,可以使用带 key 的 Fragment

function Glossary({ items }: { items: { term: string; description: string }[] }) {
  return (
    <dl>
      {items.map(item => (
        <Fragment key={item.term}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

注意,简写语法 <>...</> 不支持 key 属性,必须使用 <Fragment key={...}>

七、React 18/19 中 key 的注意事项

Strict Mode 双重渲染

React 18 的严格模式下,组件会在开发环境中渲染两次来帮助发现副作用问题。如果 key 不正确导致状态错乱,这一机制会更容易暴露问题。

Concurrent Features

React 18 的并发特性(如 useTransitionuseDeferredValue)可能会在后台准备新的 UI,如果 key 设置不当导致状态复用问题,在并发模式下会更加明显。

function SearchResults({ query }: { query: string }) {
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState<Item[]>([]);

  useEffect(() => {
    startTransition(() => {
      fetchResults(query).then(setResults);
    });
  }, [query]);

  return (
    <div style={{ opacity: isPending ? 0.7 : 1 }}>
      {results.map(item => (
        <ResultCard key={item.id} item={item} />
      ))}
    </div>
  );
}

八、总结

要点说明
key 的本质React 识别列表元素身份的标识符
Diff 作用帮助 React 建立新旧元素对应关系,实现最小化 DOM 更新
index 的问题列表重排时导致状态错乱和性能退化
最佳实践使用稳定、唯一、可预测的值作为 key
重置模式通过改变 key 强制销毁重建组件,重置内部状态
Fragment key多元素列表项必须用 <Fragment key> 而非 <>

key 看似简单,但深入理解它在 Reconciliation 中的作用机制,能够帮助开发者编写出性能更优、行为更可预测的 React 应用。