保留和重置 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!
- 用户当前选中了 Alice,在输入框里打进去了给 Alice 的悄悄话:“你好 Alice,今晚有空一起吃饭吗?”;
- 突然用户改变了主意,点击切换到了联系人 Bob;
- 可怕的事情发生了:由于
<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})。