4. 提升粒度的选择:原子状态与派生状态、细粒度提升场景

在确定了是否需要提升之后,下一个关键决策是提升什么粒度。错误的粒度选择会导致两种极端:要么是巨型状态对象(一个 useState 包含整个页面的所有数据),要么是碎片化状态(频繁创建无意义的单字段状态)。本章将建立原子状态与派生状态的区分标准,并定义细粒度提升的适用场景。

4.1 核心概念对比:原子状态 vs. 派生状态

维度 原子状态 (Atomic State) 派生状态 (Derived State)
定义 直接由用户输入或服务端写入的原始数据,不可被其他状态计算得出。 通过已有原子状态计算或组合得到的值,不独立存储。
更新方式 通过显式 setState / dispatch 触发。 跟随其依赖的原子状态自动变更。
典型例子 用户姓名、订单金额、复选框选中项 ID 列表。 筛选后的列表、计算税率后的总计、全选状态。
提升必要性 高——多子组件共享时必须提升。 低——通常直接在消费方通过 useMemo 或计算属性获得。
反模式风险 过度提升导致 root 臃肿。 将派生状态提升为原子状态,造成状态冗余与同步 Bug。
决策口诀:能算出来的就不存。只有那些无法从已有数据推导、且需要跨组件共享的原始数据,才考虑提升为共享原子状态。

4.2 派生状态提升的典型反模式

最常见的错误是将派生状态与原子状态一并提升,导致父子组件间出现“双重真相源”。

反模式示例:在父组件中同时维护 rawItems(原始列表)和 filteredItems(筛选后列表)两个状态,并在 setRawItems 后手动同步 filteredItems。这种做法极易遗漏同步时机,引发状态不一致。
// ❌ 反模式:派生状态被提升为原子状态
const [rawItems, setRawItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]); // 应使用 useMemo

// 手动同步 — 风险点:当多个 effect 竞争时容易漏更新
useEffect(() => {
  setFilteredItems(applyFilter(rawItems, filterCriteria));
}, [rawItems, filterCriteria]);
// ✅ 正确方案:原子状态提升,派生状态留在消费侧
const [rawItems, setRawItems] = useState([]);
const [filterCriteria, setFilterCriteria] = useState('');

// 仅在需要时计算,不存储
const filteredItems = useMemo(
  () => applyFilter(rawItems, filterCriteria),
  [rawItems, filterCriteria]
);

4.3 细粒度提升场景与评估维度

并非所有原子状态都需要提升到组件树的顶层。细粒度提升指的是:只提升到需要共享的最小公共祖先位置,而非一律抬升到根组件。

以下四种场景是细粒度提升的典型候选:

  1. 表单联动字段:如选择国家后,城市列表需要更新。国家 ID 提升至表单组件即可,不必到页面根。
  2. 列表项选中态:多选操作只在当前列表范围内有效,提升到列表容器组件,不扩散到父级。
  3. 分页 / 排序状态:分页器与列表共同消费当前页码,二者往往有一个公共父组件,提升到此父组件即满足。
  4. 局部模态框控制:弹窗的显隐状态通常只需要在触发按钮与 Modal 之间共享,提升到最近布局容器。

4.4 提升粒度决策表

在设计阶段,可以通过下表快速判断每个状态变量的提升范围:

评估问题 是 → 动作 否 → 动作
该状态是否由用户直接输入或服务端写入? 原子状态,考虑提升 派生状态,不提升(使用计算)
是否至少两个子组件需要读取或写入该状态? 需要提升 局域持有,不提升
共享子组件是否集中在同一子树内(深度 ≤ 3 层)? 细粒度提升到子树根 考虑是否出现跨分支共享,评估 Context 或全局状态
该状态是否仅用于条件渲染或局部动画? 应保持在最底层消费组件 按共享范围正常提升
落地建议:在开始编码前,先以组件树为单位画出每个状态变量的“共享半径”。共享半径等于 1(仅一个组件使用)则不提;共享半径大于 1 且小于等于子树范围,则提升到该子树根;共享半径跨枝跨根,再考虑全局方案。

4.5 细粒度提升的流程控制与数据流约束

当实施细粒度提升时,需要同时定义清晰的数据流边界:

  • 数据向下:提升后的状态通过 Props 向下传递,不允许子组件绕过 Props 直接修改祖先状态(除非通过回调显式授权)。
  • 控制向上:子组件通过回调函数 (onChange, onSelect) 报告事件,由提升位置的组件统一处理状态变更。
  • 职责隔离:提升位置的组件仅承担“状态托管”与“回调分发”职责,不掺入业务渲染逻辑。

4.6 本章知识图谱:提升粒度决策树

提升粒度决策树 候选状态 可由已有状态计算? 派生状态,不提升 (使用 useMemo 计算) 原子状态 需判断共享范围 共享半径 ≤ 1 个组件? 局域持有 不提 子组件在同一子树内? 细粒度提升到子树根 评估全局方案 (Context / 状态库) 细粒度提升原则:只提升到最小公共祖先,优先使用 Props + 回调,避免过早引入全局状态。 派生状态永远不提升,始终在消费侧使用 useMemo 或 computed 属性。

4.7 落地步骤与验收标准

落地步骤:

  1. 列出所有可变数据:为每个组件扫描状态变量,标记是否为原子状态。
  2. 计算共享半径:在组件树上标出每个原子状态被直接读取或写入的组件数量及分布。
  3. 应用决策表:逐一确定每个状态是局域持有、细粒度提升还是全局方案。
  4. 重构派生状态:将原本提升的派生状态替换为 useMemo 或计算属性,删除冗余存储。
  5. 编写验证测试:确保每个原子状态的更新能正确反映到所有消费组件,且派生状态不产生额外渲染。

验收标准:

  • 无状态变量同时由祖先状态和自身 set 维护(杜绝双重真相源)。
  • 每个派生状态均无对应的 useState 声明,仅通过计算获得。
  • 细粒度提升位置符合“最小公共祖先”原则,未出现状态跨多层次跳跃传递。
  • 迁移后组件渲染次数未发生异常增长(使用 React DevTools Profiler 验证)。