Zustand Store

2 min read
Zekari
React状态管理Zustand前端开发

状态是什么

状态不是数据。数据只是存在,状态是会变化的数据。

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,但它不是 useStateuseContext。它是一个订阅。

关键在于:你可以只订阅你需要的部分。

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

3 min read

数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。

React组件设计
Read More

React Context Provider

3 min read

Context 不是状态管理。它是数据传递的通道。这个区别决定了你应该如何使用它。

ReactContext API
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构设计前端开发
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构设计React
Read More

Studio 前端架构:从画布到组件的设计思考

3 min read

深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验

Purikura 项目前端架构
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目系统架构
Read More

Context 驱动的认证状态管理

3 min read

认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。

软件设计认证系统
Read More