状态结构、保留与重置:草稿应活多久
问题:选择联系人后,姓名和草稿该存几份
假设列表已有联系人对象。若 state 同时保存 selectedPerson 对象和 selectedId,联系人名称更新后,选中对象可能成为旧副本。能从已有 state/props 计算的值应在渲染时派生,只保存最小事实。
另一个问题是草稿寿命:切换联系人时,是保留每人的草稿,还是把编辑器当成新任务重置?React 根据组件在渲染树中的身份保存 state,这个选择必须写进结构。
完整实验:用 key 明确重置联系人草稿
import { useState } from 'react'
const contacts = [
{ id: 'lin', name: 'Lin', email: 'lin@example.com' },
{ id: 'ming', name: 'Ming', email: 'ming@example.com' },
]
function ContactEditor({ contact, onSave }) {
const [draft, setDraft] = useState(contact.email)
function handleSubmit(event) {
event.preventDefault()
onSave(contact.id, draft.trim())
}
return (
<form onSubmit={handleSubmit}>
<h2>编辑 {contact.name}</h2>
<input
aria-label="邮箱草稿"
value={draft}
onChange={event => setDraft(event.target.value)}
/>
<button type="submit" disabled={!draft.includes('@')}>保存</button>
</form>
)
}
export default function App() {
const [people, setPeople] = useState(contacts)
const [selectedId, setSelectedId] = useState(contacts[0].id)
const selected = people.find(person => person.id === selectedId)
function saveEmail(id, email) {
setPeople(current => current.map(person => (
person.id === id ? { ...person, email } : person
)))
}
return (
<main>
<h1>联系人</h1>
<nav>
{people.map(person => (
<button key={person.id} onClick={() => setSelectedId(person.id)}>
{person.name}
</button>
))}
</nav>
<ContactEditor
key={selected.id}
contact={selected}
onSave={saveEmail}
/>
<p>已保存:{selected.email}</p>
</main>
)
}在 Lin 的输入中写未保存内容,切到 Ming,再切回 Lin。草稿恢复为 Lin 已保存邮箱,因为 key 从 lin 变成 ming 时,旧编辑器被移除,新编辑器以新的初始 state 挂载。
state 结构先消除矛盾
selectedId 是事实,selected 是 people.find(...) 的派生结果。不要再保存 selectedName、selectedEmail 或 isSelected,否则一次联系人更新要同步多个 setter,任何遗漏都会得到互相矛盾的 UI。
设计 state 时依次问:
- 它能否由 props 或其他 state 在渲染期间直接计算?能就不存。
- 两个 state 是否总要一起变化?若是,考虑合并或用 reducer 表达转换。
- 相同实体是否重复保存为对象副本?优先保存 id,再查找当前对象。
- state 的所有者是否是所有消费者的最近共同祖先?
React 按树中身份保存 state
state 并不“住在函数里”,而由 React 关联到渲染树中的位置。通常,父位置、组件类型和 key 共同决定身份:
- 同一父位置渲染同一种组件且 key 相同,state 被保留;
- 同一位置换成另一种组件类型,原 state 被销毁;
- 同一种组件的 key 改变,也会被视为新身份并重置整个子树。
不要在组件内部定义另一个组件。外层每次渲染都会产生新的组件函数身份,内层 state 可能反复重置;把组件定义移到模块顶层。
改一个条件:去掉编辑器的 key
去掉 key={selected.id} 后,ContactEditor 始终处在同一位置、类型也相同。切到 Ming 时 React 会保留 draft,于是 Lin 的草稿出现在 Ming 编辑器里。useState(contact.email) 的初始值只在挂载时使用,prop 改变不会自动重置 state。
如果产品要求“每个联系人都保留未保存草稿”,不能只去掉 key。应把草稿提升为父组件中的 { [contactId]: email } 映射,使每份草稿由稳定 id 管理;编辑器变成受控组件。
边界
- key 重置会销毁整棵子树的局部 state、DOM 状态和订阅;只想改一个字段时,显式 setter 更精确。
- 把 prop 复制进 state 通常会过期;只有“从初始值开始、之后独立编辑”这类语义才合理,并应明确何时重置。
- 隐藏方式不同,生命周期也不同:从树中条件移除会卸载;CSS 隐藏仍保留组件。
- 派生计算若昂贵,先保证不重复存真相;性能优化不能以制造双份 state 为代价。
练习:按项目隔离的搜索草稿
实现一个 App.jsx,有两个项目 { id, name, items } 和一个 SearchPanel。用户切换项目时,每个项目的查询草稿都要保留;搜索结果由项目数据与当前草稿派生,不能存进 state。
契约:
- 父组件 state 只保存
selectedId与drafts映射; SearchPanel是受控组件,接收query、onQueryChange与项目;- 搜索大小写不敏感,空查询显示全部;无结果显示“无匹配项”;
- 切换 A→B→A 后恢复 A 的草稿;项目名称变化时选中关系仍靠 id,不保存选中对象副本。
完成后对照答案 03。