Skip to content

状态结构、保留与重置:草稿应活多久 ​

问题:选择联系人后,姓名和草稿该存几份 ​

假设列表已有联系人对象。若 state 同时保存 selectedPerson 对象和 selectedId,联系人名称更新后,选中对象可能成为旧副本。能从已有 state/props 计算的值应在渲染时派生,只保存最小事实。

另一个问题是草稿寿命:切换联系人时,是保留每人的草稿,还是把编辑器当成新任务重置?React 根据组件在渲染树中的身份保存 state,这个选择必须写进结构。

完整实验:用 key 明确重置联系人草稿 ​

jsx
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 时依次问:

  1. 它能否由 props 或其他 state 在渲染期间直接计算?能就不存。
  2. 两个 state 是否总要一起变化?若是,考虑合并或用 reducer 表达转换。
  3. 相同实体是否重复保存为对象副本?优先保存 id,再查找当前对象。
  4. 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。

下一章 ​

状态架构、Reducer 与 Context

用心记录每一步成长