Zustand 的实际体验:为什么我放弃了 Redux
Zustand 的实际体验为什么我放弃了 Redux样板代码的重量React 应用的状态管理通常是从useState开始的。项目初期够用但随着业务增长跨组件共享状态的需求出现了。这时候 Redux 是个自然的选择——但代价是什么为了一个简单的计数器你需要写 action type、action creator、reducer、selector、middleware。五个文件数百行代码只为更新一个数字。这不是说 Redux 不好。单一数据源、纯函数 reducer、可预测的状态变更——这些在大型团队中确实有价值。但对于独立开发者或小型项目样板代码的成本远超收益。每次状态变更都要走 dispatch → middleware → reducer → selector 这一整条链路开发体验确实沉重。Zustand 的思路不同用最少的 API 覆盖 90% 的场景。它不追求架构完备而是追求使用极简。响应式订阅模型Zustand 的核心 API 只有三个create创建 store、set更新状态、get读取状态。背后是基于发布-订阅模式的响应式系统。sequenceDiagram participant C as React 组件 participant S as Zustand Store participant L as Listener 队列 C-S: useStore(selector) S-S: 执行 selector(state) 获取切片 S-L: 注册当前组件为 listener Note over S,L: 组件挂载时订阅卸载时自动移除 C-S: set({ count: 1 }) S-S: 浅合并状态生成 nextState S-L: 遍历所有 listener L-L: 对每个 listener 执行 selector(nextState) L-L: 对比 selector 返回值是否变化Object.is L--C: 仅当切片变化时触发重渲染 Note over L,C: 精确订阅未变化的组件不渲染和 Redux 的关键差异在于Redux 是中心化广播——每次 dispatch 触发所有useSelector执行由 selector 决定是否重渲染Zustand 是切片订阅——每个组件只订阅自己关心的状态切片状态变更时只通知相关订阅者。小型应用里这种差异几乎无感。但状态树庞大、组件层级深的时候Redux 的广播模式会导致大量无效的 selector 计算。Zustand 的切片订阅天然避免了这个问题不需要手动优化 selector 的 memoization。另一个细节是set方法。它默认执行浅合并shallow merge而不是 Redux 的全量替换。这意味着你不需要展开运算符来保留未变更的字段// Redux reducer必须手动展开 case UPDATE_USER: return { ...state, name: action.payload } // Zustand set自动浅合并 set({ name: New Name })这减少了遗漏字段导致的 Bug也降低了认知负担。生产实践下面是一个实际项目的状态管理方案包含异步操作、持久化存储和跨 Store 通信import { create } from zustand import { persist, createJSONStorage } from zustand/middleware import { immer } from zustand/middleware/immer import { shallow } from zustand/shallow interface UserState { user: { id: string name: string avatar: string plan: free | pro } | null token: string | null login: (credentials: { email: string; password: string }) Promisevoid logout: () void updateAvatar: (url: string) void } const useUserStore createUserState()( persist( immer((set, get) ({ user: null, token: null, login: async (credentials) { try { const res await fetch(/api/auth/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(credentials), }) if (!res.ok) { const err await res.json().catch(() ({})) throw new Error(err.message || 登录失败HTTP ${res.status}) } const data await res.json() set((state) { state.user data.user state.token data.token }) } catch (error) { set({ user: null, token: null }) throw error } }, logout: () { set((state) { state.user null state.token null }) localStorage.removeItem(auth-token) }, updateAvatar: (url) { set((state) { if (state.user) { state.user.avatar url } }) }, })), { name: user-storage, storage: createJSONStorage(() localStorage), partialize: (state) ({ token: state.token }), } ) ) interface CanvasState { elements: Array{ id: string type: rect | circle | text x: number y: number width: number height: number fill: string } selectedId: string | null zoom: number addElement: (element: CanvasState[elements][0]) void moveElement: (id: string, x: number, y: number) void selectElement: (id: string | null) void setZoom: (zoom: number) void } const useCanvasStore createCanvasState()( immer((set) ({ elements: [], selectedId: null, zoom: 1, addElement: (element) { set((state) { state.elements.push(element) }) }, moveElement: (id, x, y) { set((state) { const el state.elements.find((e) e.id id) if (el) { el.x x el.y y } }) }, selectElement: (id) { set({ selectedId: id }) }, setZoom: (zoom) { set({ zoom: Math.max(0.1, Math.min(5, zoom)) }) }, })) ) function useEditorState() { const user useUserStore((s) s.user, shallow) const selectedId useCanvasStore((s) s.selectedId) const elements useCanvasStore((s) s.elements) const zoom useCanvasStore((s) s.zoom) return { user, selectedId, elements, zoom, selectedElement: elements.find((e) e.id selectedId) ?? null, canEdit: user?.plan pro, } }中间件组合顺序要注意persist必须在最外层确保状态先经过 immer 处理再持久化immer 在内层保证set回调中的 mutable 写法能正确转换为不可变更新。顺序反了会导致持久化存储的是 Immer Draft 对象而非正常状态。Zustand 不适合什么Zustand 的极简设计在某些场景下会暴露短板。时间旅行调试是首要缺失。Redux DevTools 的时间旅行功能允许回溯任意历史状态对排查复杂的状态 Bug 很有效。Zustand 虽然可以通过devtools中间件接入 DevTools但只支持 action 日志查看不支持完整的时间旅行回溯。如果应用状态变更逻辑极其复杂需要频繁回溯调试Redux 的可追溯性优势不可替代。严格的单向数据流约束是另一个差异。Redux 的 dispatch → reducer 流程是强制的任何状态变更都必须经过 reducer 纯函数处理这在团队协作中提供了天然的状态变更审计能力。Zustand 的set方法可以在任何地方直接调用灵活性高但也意味着状态变更的入口不受约束。在多人协作的大型项目中缺乏统一的状态变更入口可能导致状态更新逻辑散落各处增加维护成本。中间件生态的成熟度差距明显。Redux 有丰富的中间件生态redux-saga 处理复杂异步、redux-observable 基于 RxJS 实现响应式流、redux-persist 提供多引擎持久化。Zustand 的中间件体系相对简单虽然支持自定义中间件但社区贡献的成熟方案较少。如果项目依赖特定的 Redux 中间件能力迁移成本需要仔细评估。维度ZustandRedux Toolkit样板代码量极少较多包体积~1.2 KB gzipped~11 KB gzipped时间旅行有限支持完整支持异步处理原生 async/awaitcreateAsyncThunk团队约束弱强学习曲线低中等实际建议Zustand 的价值不在于替代 Redux而在于为中小型项目提供了一种刚刚好的状态管理方案。切片订阅避免无效渲染immer 中间件简化不可变更新persist 中间件解决持久化需求。对于独立开发者这种设计意味着更少的代码、更少的 Bug、更快的迭代速度。我的建议新项目优先尝试 Zustand。当状态复杂度增长到需要严格的变更审计和时间旅行调试时再考虑迁移至 Redux Toolkit。状态管理工具的选择应该跟随项目复杂度演进而不是预判未来需求提前引入重量级方案。