0%

已完成 0/29 关

教学模式

你可以随时调整。

此设置调整教师的教学方式,无需重新生成课程。

第 23 关 含代码 约 12 分钟

保留和重置 State:组件在 UI 树中的位置与 key 的作用

保留和重置 State:组件在 UI 树中的位置与 key 的作用

很多初学者直觉上认为:既然 useState 写在 Counter 函数内部,那么状态肯定就保存在这个函数或者组件实例身上吧?

这个直觉是错误的! 在 React 中,State 并不是存放在组件函数内部的,而是保存在 React 的底层数据结构中。React 依靠组件在 UI 渲染树中的确切位置,来决定把哪一份状态分配给哪个组件。

理解了这一点,你就能完全掌控组件状态何时应该被保留,何时应该被彻底清空重置。


1. 核心法则:相同位置的相同组件,保留 State

观察下面这个计数器组件:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);

  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} />
      ) : (
        <Counter isFancy={false} />
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => setIsFancy(e.target.checked)}
        />
        使用华丽样式
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  return (
    <div className={isFancy ? 'fancy' : ''}>
      <h1>得分: {score}</h1>
      <button onClick={() => setScore(score + 1)}>加分</button>
    </div>
  );
}

现场推演:

假设你点击按钮把分数加到了 5。然后你勾选了“使用华丽样式”的复选框。 此时界面的得分会变成 0 吗? 不会!得分依然完好无损地保持在 5!

为什么状态保留下来了?

虽然你的 JSX 源码里写了两个看似独立的 <Counter /> 标签,但在 React 看来:

  • 在外层 <div> 的第一个子节点位置上,上一次渲染的是一个 Counter 组件;
  • 本次渲染在完全相同的树位置上,依然是一个 Counter 组件(组件类型没有变)!
  • 对 React 而言,这就是同一个组件在同一个坑位上,只是换了个样式 prop 重新渲染而已。因此,React 完好地保留了它在这个位置上的状态!

2. 相同位置的不同组件,彻底销毁重建 State

如果同一个位置上的组件类型变了(比如原本是个 <Counter />,后来变成了 <p>,或者变成了另一个不同的组件 <ScoreBoard />):

<div>
  {isPaused ? (
    <p>游戏暂停</p>
  ) : (
    <Counter />
  )}
</div>

当 isPaused 从 false 变成 true,再变回 false 时: React 会在切换走的那一瞬间,把原位置组件的整个状态树彻底连根拔起并销毁(Unmount)! 当 Counter 再次出现时,它是一个从零重生的新组件,内部状态重新以初始值 0 开始。


3. 痛点:如何强制在相同位置重置 State?

现在看一个非常贴近日常真实业务的场景: 我们正在开发一个即时聊天应用。页面上方可以选择联系人列表(Alice 或 Bob),下方是一个文本输入框,用来给选中的人写信:

export default function Messenger() {
  const [to, setTo] = useState('Alice');

  return (
    <div>
      <ContactList selectedContact={to} onSelect={contact => setTo(contact)} />
      <Chat contact={to} />
    </div>
  );
}

function Chat({ contact }) {
  const [text, setText] = useState(''); // 草稿输入框

  return (
    <section>
      <textarea
        value={text}
        placeholder={'给 ' + contact + ' 写消息...'}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button>发送给 {contact}</button>
    </section>
  );
}

发生了极其尴尬的严重 Bug!

  1. 用户当前选中了 Alice,在输入框里打进去了给 Alice 的悄悄话:“你好 Alice,今晚有空一起吃饭吗?”;
  2. 突然用户改变了主意,点击切换到了联系人 Bob;
  3. 可怕的事情发生了:由于 <Chat contact={to} /> 始终处在同一树节点位置上,React 保留了它的 text 状态!原本打算发给 Alice 的表白草稿,居然毫无保留地出现在了给 Bob 的输入框里!用户只要不留神按下发送,就会发送给错误的人!

4. 救星登场:使用 key 显式命令 React 重置状态

我们希望:只要联系人变了,下面的 Chat 组件就应该被视为一个全新的聊天室,输入框草稿必须被彻底清空!

如何告诉 React“虽然位置没变,但这已经不是同一个组件了”? 答案是:给组件添加一个独特的 key 属性!

export default function Messenger() {
  const [to, setTo] = useState('Alice');

  return (
    <div>
      <ContactList selectedContact={to} onSelect={contact => setTo(contact)} />
      {/* 赋予 key={to} */}
      <Chat key={to} contact={to} />
    </div>
  );
}

奇迹发生了:

当 to 从 'Alice' 变成 'Bob' 时,React 看到 key 从 'Alice' 变成了 'Bob'。 对于不同的 key,React 会断定它们是两个完全不同的独立实体! React 会立刻销毁属于 Alice 的旧 Chat 组件(连带清空旧草稿),并在原地全新挂载一个属于 Bob 的全新 Chat 组件,输入框干干净净!


总结

  • State 储存在 React 内部,并由组件在 UI 树中的位置决定归属;
  • 只要相同位置的组件类型不变,React 就会持久保留它的状态;
  • 如果相同位置的组件类型发生改变,旧组件的状态会被立即销毁;
  • 当需要在相同位置强制清空重置组件状态时,给组件指定一个随数据变化的 key 属性(如 key={userId})。
下一站:迁移状态逻辑至 Reducer:dispatch 与 useReducer 重构 →
© 2026 React 官方教程