Zustand Store
状态是什么
状态不是数据。数据只是存在,状态是会变化的数据。
React 组件的核心矛盾在于:组件本身是纯函数,但它需要处理会变化的东西。props 从外部传入,state 在内部存在。这个边界决定了组件的复杂度。
当状态只属于一个组件时,一切都很简单。但现实中,状态总是需要被共享。父组件把状态传给子组件,兄弟组件通过共同的父组件通信。这个过程被称为"状态提升"。
状态提升是正确的,但不总是优雅的。
💡 Click the maximize icon to view in fullscreen
图中灰色的中间层只是传递状态。它们不使用这些数据,只是把数据向下传。这就是 prop drilling。
为什么需要全局状态
当两个距离很远的组件需要共享状态时,你有三个选择:
一是把状态提升到它们共同的父组件。但如果这个父组件在很上层,中间的所有组件都要传递 props。
二是使用 Context。Context 让你跳过中间层,直接传递数据。但 Context 的问题是:任何 Context 的改变都会导致所有使用它的组件重新渲染。
三是使用外部状态管理。状态不再属于组件树,而是独立存在。组件订阅状态,状态变化时组件更新。
Zustand 选择了第三条路。
Zustand 的核心思想
Zustand 只做一件事:让你在 React 之外创建一个 store,然后在组件中订阅它。
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
这是一个 store。它不是组件,不是 Context,只是一个普通的对象。
function Counter() {
const count = useStore((state) => state.count)
const increase = useStore((state) => state.increase)
return <button onClick={increase}>{count}</button>
}
这是使用 store 的组件。useStore 是一个 hook,但它不是 useState 或 useContext。它是一个订阅。
关键在于:你可以只订阅你需要的部分。
const count = useStore((state) => state.count)
这个组件只关心 count。如果 store 中其他的值变化了,这个组件不会重新渲染。这是 Zustand 和 Context 的根本区别。
性能优化的核心不是让程序运行得更快,而是让它少做无用的事情。
Context 会导致所有消费者重新渲染,因为 React 不知道你用了 Context 中的哪个值。
Zustand 通过 selector 函数让你明确订阅什么。React 只在你订阅的部分变化时才更新组件。
这不是魔法,只是更精确的订阅。
状态的边界
全局状态听起来很方便,但它有代价。
状态的作用域越大,维护就越难。全局状态意味着任何地方都可以修改它,任何地方都可能依赖它。这增加了心智负担。
Zustand 不强迫你把所有状态都放在一个全局 store 里。你可以创建多个 store:
const useUserStore = create(...)
const useCartStore = create(...)
const useUIStore = create(...)
每个 store 管理自己领域的状态。用户信息、购物车、UI 状态——它们的生命周期不同,依赖关系不同,不应该混在一起。
这是模块化的本质。不是把所有东西放在一个地方,而是把相关的东西放在一起。
💡 Click the maximize icon to view in fullscreen
组件可以同时订阅多个 store。每个 store 独立更新,互不干扰。
何时使用 Zustand
不是所有状态都需要 Zustand。
表单的输入值、模态框的开关、标签页的选中状态——这些局部状态用 useState 就够了。它们只属于一个组件,不需要共享。
但当状态需要跨越多个层级,或者在多个不相关的组件之间共享时,Zustand 就有价值了。
判断标准很简单:如果你发现自己在传递很多层 props,或者在写很深的 Context 嵌套,那就是信号。
Zustand 不是为了取代 useState,而是为了解决 useState 解决不了的问题。
Zustand 支持中间件系统,可以添加各种功能:
- persist:把状态保存到 localStorage
- immer:使用 Immer 简化不可变更新
- devtools:集成 Redux DevTools
但这些都是可选的。Zustand 的核心非常小,只有状态管理的基本逻辑。
这反映了一个设计哲学:核心功能要简单,扩展功能通过插件实现。
你不需要学习整个生态系统才能开始使用。你只需要理解 create 和 useStore。其他的,等你需要的时候再学。
简单的力量
Redux 很强大。它有单向数据流、reducer、action、中间件、time travel debugging。但这些能力有代价——学习曲线和样板代码。
对于大型应用,这些复杂度是值得的。但对于中小型项目,它们可能是负担。
Zustand 的设计理念是:给你足够的能力,但不强迫你使用不需要的功能。
没有 action types,没有 reducer,没有 dispatch。你直接调用函数修改状态:
const useStore = create((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text }]
})),
}))
这不是为了炫技,而是为了减少中间层。每一层抽象都有成本——理解成本、维护成本、调试成本。
当你不需要时间旅行调试时,为什么要为它付出代价?
最后
状态管理的本质问题不是选择哪个库,而是理解状态的边界。
哪些状态应该是局部的?哪些应该是共享的?哪些应该持久化?
这些问题的答案决定了架构。工具只是实现架构的手段。
Zustand 提供了一种轻量的方式来管理共享状态。它不是银弹,但在很多场景下,它够用了。
够用,有时候就是最好的。
参考资源
Related Posts
Articles you might also find interesting
Props Drilling
数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。
React Context Provider
Context 不是状态管理。它是数据传递的通道。这个区别决定了你应该如何使用它。
离屏渲染:照片捕获为什么需要独立的 canvas
实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。
Purikura的页面系统
通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。
Studio 前端架构:从画布到组件的设计思考
深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验
Studio 系统架构:从状态机到端到端流程
深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。