← [ BACK_TO_NOTES ]

Zustand 全局状态管理

Zustand 的核心用法:用轻量 store 管理共享客户端状态、选择性订阅与可选持久化,并与 TanStack Query 的 server-state projection 做清晰 ownership 分工。

#howto #growing #frontend #react

[!info] related notes

Zustand 全局状态管理

核心问题

Zustand 适合管理需要跨组件/跨页面共享的客户端状态,例如 UI 偏好、工作流状态、临时客户端 runtime state,以及某些需要统一协调的客户端凭据状态。

关键不是“数据能不能塞进 Zustand”,而是:

这份状态的 owner 是谁、生命周期多长、需要谁消费,以及冲突时谁裁决?

Zustand 是一个 state container,不会因为数据被放进去就自动成为业务 source of truth。

最小用法

interface UIState {
  sidebarCollapsed: boolean;
  setSidebarCollapsed: (value: boolean) => void;
}

export const useUIStore = create<UIState>()((set) => ({
  sidebarCollapsed: false,
  setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }),
}));

组件中使用选择性订阅:

const collapsed = useUIStore((state) => state.sidebarCollapsed);
const setCollapsed = useUIStore((state) => state.setSidebarCollapsed);

Global 不等于 Durable,也不等于 Server Truth

Global store
= 客户端很多地方都能访问

Durable truth
= 系统最终认定的业务事实

两者不是一回事。

例如 /api/me 返回:

{ "id": "u1", "name": "Alice" }

即使为了认证流程方便把这份 user projection 放进 Zustand:

Zustand user = Alice

它的业务 ownership 仍属于服务器。若服务器已变为 Bob,而 Zustand 还保留 Alice,应重新同步服务器,而不是认为“Zustand 是全局状态,所以 Alice 才是真的”。

持久化:Storage Policy,不是 Ownership Rule

Zustand 的 persist 中间件适合保存安全且确实应该跨刷新/重启保留的客户端状态,例如主题或布局偏好:

const usePreferencesStore = create<PreferencesState>()(
  persist(
    (set) => ({
      sidebarCollapsed: false,
      setSidebarCollapsed: (value) => set({ sidebarCollapsed: value }),
    }),
    {
      name: 'ui-preferences',
      partialize: (state) => ({
        sidebarCollapsed: state.sidebarCollapsed,
      }),
    },
  ),
);

不要把“需要 localStorage”当成选择 Zustand 的充分条件,也不要因为 Zustand 支持 persist 就默认把认证 bearer secrets 放进 localStorage。

特别是认证设计要单独评估 XSS、刷新恢复、Cookie/HttpOnly、服务端 Session Authority 等安全边界。见 Cookie、Session 与 Token。

BodySense 当前的典型模型是:

access token
→ 客户端内存状态

refresh credential
→ HttpOnly Cookie

server session authority
→ server/Redis

而不是在 Zustand persist 中把 access/refresh token 长期写入 localStorage。

选择性订阅

// ✅ 只订阅需要的字段
const user = useAuthStore((state) => state.user);

// ⚠️ 订阅整个 store 会让更多变化触发当前组件更新
const store = useAuthStore();

Zustand 的 selector 让消费者只关心自己需要的 slice;这属于客户端渲染/订阅优化,不改变状态 ownership。

与 TanStack Query 的分工

不要背:

来自 API → TanStack Query
需要 localStorage → Zustand

更准确的判断是:

Server-owned state
→ TanStack Query 通常最适合负责 fetch/cache/stale/refetch 生命周期

Shared client-owned state
→ Zustand / Context / Redux 等

Local component-only state
→ useState / useReducer

URL-addressable identity/state
→ Router / URL

例如:

会话列表 / HealthWorkspace
→ server-owned projection → TanStack Query

Sidebar collapsed
→ shared client presentation → Zustand

Dialog open
→ local component state → useState

conversationId in /conversations/:id
→ URL state

如果确实因为流程需要把 server-derived data 镜像到 Zustand,也要明确它是 projection,并定义刷新/失效策略,避免与 Query/服务器形成两个可独立修改的 truth source。

什么情况下 Global State 才值得存在

可以问:

1. 是否真的有多个远距离消费者?
2. 状态是否需要跨 route/component lifetime?
3. 由谁修改,修改规则是否集中?
4. 能否从 URL、Query data 或 props 派生?
5. 放全局后会不会形成第二份 server state?

能局部化或派生的状态,不要因为“以后可能会用”就提前全局化。

认证 Store 的特殊性

认证经常混合了几种不同 ownership:

accessToken
→ client runtime credential

user profile
→ server-derived projection

isAuthenticated
→ 通常是认证流程的客户端 projection

server session liveness
→ server authority

所以一个 authStore 可以为了流程协调把它们放在一起,但不代表这些字段属于同一 authority layer。

常见错误

把 API response 全复制进 Zustand

TanStack Query has user/workspace
+
Zustand has independently mutable copy
→ drift

除非有明确的 draft/transient projection 语义,否则避免双写。

用 persist 长期保存敏感 token,只因为实现方便

Storage 必须服从安全模型,不是服从状态库能力。

把局部 UI state 过度全局化

Dialog/Accordion 等只属于一个局部交互时,useState 往往更简单、更符合 lifetime。

在组件外调用 React hook

组件/Hook 外若需要 imperative 访问,可按 Zustand API 使用:

const { accessToken } = useAuthStore.getState();

而不是在普通函数中调用 React selector hook。

最短记忆

Zustand = shared client-state container
TanStack Query = server-state synchronization/cache
URL = navigation identity
useState = local interaction
Server = durable business authority

Persist 只是 storage policy;Global 只是 scope;
它们都不等于 source of truth。
创建于 2026/6/25 更新于 2026/9/10
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本