6-15.【架构设计】State和Derived State(派生状态)应该如何区分?错误区分会带来哪些 一致性和性能问题?

16 阅读3分钟

这是一个所有“状态驱动 UI”体系的核心问题
如果把它搞清楚,SwiftUI / TCA / Redux / Jetpack Compose 的 80% 性能与一致性问题都会自动消失

  1. 什么是 State vs Derived State(工程定义)
  2. 最小判定法则(实用)
  3. 错误区分会导致的“一致性灾难”
  4. 错误区分导致的“性能陷阱”
  5. SwiftUI / TCA 中的落地策略

一、先给一句工程级定义(不是教科书)

✅ State(源状态)

State = 系统必须“记住”的最小事实集合

  • 不可从其他状态推导
  • 丢失就无法恢复
  • 生命周期明确

✅ Derived State(派生状态)

Derived State = 从 State 计算得出的视图或业务投影

  • 可 100% 从 State 重新算出
  • 不需要被“记住”
  • 只为方便使用 / 性能
State ──计算──▶ Derived State

二、一个“不会错”的判定法则(强烈建议背下来)

如果删掉这个字段,只靠其他 State 能 100% 还原它,它就不是 State。


三、经典错误一:把 Derived State 当成 State(最常见)

❌ 错误示例

struct State {
    var items: [Item]
    var totalPrice: Double   // ❌ Derived
}

totalPrice 明明可以:

items.reduce(0) { $0 + $1.price }

一致性灾难如何发生?

Action A → 改 items
❌ 忘了改 totalPrice

或者:

Action B → 改 totalPrice
❌ items 没变
结果:
  • UI 显示错
  • Bug 偶现
  • Debug 极难

👉 双源真相(Two Sources of Truth)


四、经典错误二:把 State 当 Derived State(隐蔽但致命)

❌ 错误示例

var filteredUsers: [User] {
    users.filter { $0.name.contains(searchText) }
}

然后在 body 里:

List(filteredUsers) { ... }

看似没问题,但:

  • filter 很重
  • 每次 diff 都会算
  • 输入就卡

五、错误区分导致的两类灾难


🧨 灾难一:一致性问题(逻辑层)

症状
  • 同一数据在不同页面值不同
  • UI 偶尔“自己修复”
  • 重进页面就好了
根因

Derived State 被持久化,且更新路径不唯一


🧨 灾难二:性能问题(渲染层)

症状
  • SwiftUI 输入卡顿
  • List 滚动掉帧
  • CPU 异常
根因

Derived State 在 body / 高频路径中重复计算


六、那 Derived State 到底应该放哪?

答案不是“不要”,而是:

放在“离使用者最近,但不参与一致性”的地方


策略一:即时计算(轻量)

var isValid: Bool {
    email.contains("@")
}
  • 计算便宜
  • 不存

策略二:缓存为局部 State(性能优化)

@State private var filtered: [User] = []

.onChange(of: searchText) {
    filtered = users.filter(...)
}

👉 Derived State,但显式缓存


策略三:ViewModel / Adapter 层

final class ViewModel {
    let users: [User]
    var filteredUsers: [User] { ... }
}
  • SwiftUI body 干净
  • 性能可控

策略四:TCA 中的 computed / Selector

var totalPrice: Double {
    state.items.reduce(...)
}

或 memoized selector。


七、SwiftUI 里的黄金规则(非常重要)

@State 只能用于“用户直接操纵的最小事实”

不是:

  • 显示文本
  • 排序结果
  • 是否可点击(通常 derived)

而是:

  • 输入值
  • 选择项
  • 展开 / 折叠
  • 当前焦点

八、一个真实“修复即提速”的案例

❌ 改前

@State var sortedItems: [Item]
  • 每次数据源变化都要同步
  • 状态错乱

✅ 改后

var sortedItems: [Item] {
    items.sorted(by: ...)
}

或缓存到 ViewModel。

👉 一致性消失 + diff 成本下降。


九、终极心法(适用于任何架构)

State 越少,系统越稳定;
Derived State 越靠近消费端,系统越快。


最后一句话(架构级)

State 是“必须记住的事实”,
Derived State 是“可以随时算出的观点”。
混淆它们,系统就会同时变慢、变乱、变不可维护。

英文版

6-15. [Architecture Design] State vs. Derived State: How to Distinguish Them? What Consistency and Performance Issues Arise from Misclassification?

This is the core question of any "State-Driven UI" system. If you master this distinction, 80% of performance and consistency issues in SwiftUI, TCA, Redux, and Jetpack Compose will disappear automatically.


I. Engineering Definitions (The Practical Version)

✅ State (Source of Truth)

State = The minimal set of facts that the system must "remember."

  • Non-derivable: It cannot be computed from other pieces of data.
  • Irreplaceable: If lost, it cannot be recovered.
  • Defined Lifecycle: It has a clear beginning and end tied to a feature or the app.

✅ Derived State (Projected State)

Derived State = A view or business projection computed from the State.

  • 100% Recomputable: Can be recreated entirely from the Source State.
  • Transient: Does not need to be "stored" or "remembered" long-term.
  • Purpose-Driven: Created for convenience or performance optimization.

II. The "Bulletproof" Test (Memorize This!)

"If you delete this field and can still 100% reconstruct it using only the remaining State, then it is NOT a State—it is a Derived State."


III. Classic Error 1: Treating Derived State as State (Most Common)

❌ Incorrect Example

Swift

struct OrderState {
    var items: [Item]
    var totalPrice: Double   // ❌ This is Derived!
}

totalPrice can clearly be derived via: items.reduce(0) { $0 + $1.price }

How the "Consistency Disaster" Happens:

  1. Action A modifies items but forgets to update totalPrice.
  2. Action B modifies totalPrice but the items list remains unchanged.

Result:

  • The UI displays conflicting information.
  • Bugs appear intermittently (Heisenbugs).
  • Debugging becomes a nightmare because you have Two Sources of Truth.

IV. Classic Error 2: Treating State as Derived State (Subtle but Fatal)

❌ Incorrect Example

Swift

var filteredUsers: [User] {
    users.filter { $0.name.contains(searchText) } // ❌ Expensive computation in body
}

Then, inside the body: List(filteredUsers) { ... }

The Problem:

  • The filter operation is "heavy."
  • It runs on every single diff/re-render.
  • The UI stutters during every keystroke (Typing Lag).

V. Two Categories of Disasters

🧨 Disaster 1: Consistency Issues (Logic Layer)

  • Symptoms: The same data shows different values on different pages; the UI "self-heals" only after a refresh or page reentry.
  • Root Cause: Derived State was persisted (stored), and the update paths diverged.

🧨 Disaster 2: Performance Issues (Rendering Layer)

  • Symptoms: Laggy input in SwiftUI, dropped frames during List scrolling, abnormal CPU spikes.
  • Root Cause: Derived State is being redundantly recalculated in the body or high-frequency execution paths.

VI. Where Should Derived State Live?

The answer isn't "don't use it," but rather: "Put it as close to the consumer as possible, without letting it participate in the Source of Truth."

Strategy 1: Just-In-Time Computation (Lightweight)

Swift

var isValid: Bool {
    email.contains("@")
}
  • Cost: Cheap calculation.
  • Storage: None.

Strategy 2: Explicit Caching/Memoization (Performance Optimization)

Swift

@State private var filtered: [User] = []

.onChange(of: searchText) { newValue in
    filtered = users.filter { ... }
}

👉 Technically Derived State, but explicitly cached to prevent re-filtering on every render.

Strategy 3: ViewModel / Adapter Layer

Swift

final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    var filteredUsers: [User] { ... } // Keeps SwiftUI body clean
}

Strategy 4: Computed Properties or Selectors in TCA

Utilize computed properties in the State struct or use Memoized Selectors to ensure the computation only runs when the source data actually changes.


VII. The Golden Rule of SwiftUI

@State should only be used for the "Minimal Facts directly manipulated by the user."

Avoid using @State for:

  • Formatted display text.
  • Sorted/Filtered results.
  • "Is Enabled" flags (usually derived from other data).

Do use @State for:

  • Raw input strings.
  • Selected index/ID.
  • Expanded/Collapsed toggle states.
  • Current focus/First responder.

VIII. A Real-World "Fix-to-Speed-Up" Case

❌ Before

@State var sortedItems: [Item]

  • Pain: Requires synchronization every time the data source changes; state often gets out of sync.

✅ After

var sortedItems: [Item] { items.sorted(...) } (or cached in ViewModel).

  • Result: Consistency bugs vanish + Diffing costs decrease.

IX. Ultimate Mental Model

The less State you have, the more stable the system.

The closer the Derived State is to the consumer, the faster the system.


Final Architectural Takeaway

State is a "Fact that must be remembered."

Derived State is an "Opinion that can be recalculated at any time."

Confuse the two, and your system will simultaneously become slower, messier, and unmaintainable.