• Zustand库-一款小巧精悍的状态管理工具
  • 发布于 18小时前
  • 49 热度
    0 评论
状态管理一直是前端开发绕不开的话题。用过 Redux 的朋友都懂,它虽然功能强大、生态丰富,但搭配 TypeScript 写起来实在太繁琐。一旦项目复杂一点,你得手动写一堆模板代码:Action 类型、Reducer 分支、Store 配置、Dispatch、Selector,再加上 TS 的类型声明……光是搭一套结构就能忙活半天了。直到我接触了 Zustand,一个更符合 React 思维的状态管理库,我才发现——原来共享状态也能这么轻爽。

这篇文章就结合我自己做的 demo 项目,带你完整体验一下 Zustand + TS 的状态管理流程。

🚀 Zustand 是什么?为什么要选择它?
Zustand 是由 React Three Fiber 团队开发的一个轻量级、基于 hooks 的状态管理库,它的设计哲学是:用最小的 API,实现最大的效率。你不需要 Provider,也不需要定义 Action、Reducer,只用一个 create() 函数就能搞定状态管理。在使用前,它给我的第一印象,就是 Vue 的组合式 API + Pinia 的融合体。

选它的理由很简单:
.写法简单,学习成本低
.完美支持 TypeScript 类型推导
.不依赖 context,不会出现 Provider 嵌套地狱
.按需订阅,性能好,不会整个组件树重渲染
.支持状态持久化、中间件扩展、状态模块拆分
可以说,它正好填补了 Redux 太重、useContext 太弱之间的那块空白区域。

🎯 搞个简单案例练练手
想象一下你正在写一个后台管理系统,支持“多页签”切换。每个 tab 代表一个打开的模块(比如用户列表、设置页等),我们希望:
.支持新增和关闭 tab
.能记录当前激活的 tab

.所有页面都能访问和修改 tab 状态


如果用 Redux 来实现这些,得:
.定义 TabState 接口
.写 reducer 和 action type
.用 immer 管理不可变数据
.加 middleware 管异步
.用 hooks 或 connect 绑定组件
太繁琐了,不如用 Zustand 搞个 demo,十几行代码就能搞定共享状态。

第一步:创建 Store
我们先来定义一下状态结构,使用 TypeScript 写一个接口:
// 定义单个 tab 的结构
interface TabItem {
  id: string
  title: string
}
 // 堆代码 duidaima.com
// 整个 store 的结构
interface TabState {
  tabs: TabItem[]
  currentTabId: string
  addTab: (tab: TabItem) =>void
  removeTab: (id: string) =>void
  setCurrentTab: (id: string) =>void
}
然后用 Zustand 创建一个 store:
import { create } from 'zustand'

export const useTabStore = create<TabState>((set) => ({
  tabs: [],
  currentTabId: '',
  addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab] })),
  removeTab: (id) => set((state) => ({
    tabs: state.tabs.filter((t) => t.id !== id),
    currentTabId: state.currentTabId === id ? '' : state.currentTabId
  })),
  setCurrentTab: (id) => set({ currentTabId: id })
}))
是不是很简洁?一个 create() 函数就搞定共享状态,类型提示也直接到位。

第二步:页面中使用
接下来我们在页面中使用它。
import { useTabStore } from'./store/tabStore'
exportdefaultfunction TabView() {
const { tabs, currentTabId, addTab, setCurrentTab } = useTabStore()

return (
    <div>
      <button onClick={() => addTab({ id: 'settings', title: '设置' })}>
        新增 Tab
      </button>
      <div style={{ display: 'flex', gap: 8 }}>
        {tabs.map((tab) => (
          <div
            key={tab.id}
            onClick={() => setCurrentTab(tab.id)}
            style={{
              padding: 4,
              borderBottom: tab.id === currentTabId ? '2px solid blue' : 'none',
              cursor: 'pointer'
            }}
          >
            {tab.title}
          </div>
        ))}
      </div>
    </div>
  )
}
现在这个组件可以随时新增 Tab、切换当前 Tab,所有状态都由 Zustand 管理,其他组件也能随时访问它,非常方便。

✨ 你可能不知道的 Zustand 高级特性
除了基本的状态管理,Zustand 还提供了很多高级功能,比如:精准订阅,避免无效渲染
举个栗子:
假设我们有一个管理用户信息和主题设置的 store:
import { create } from'zustand';

// 创建一个包含多个字段的状态
const useAppStore = create((set) => ({
// 用户信息
  user: {
    name: '张三',
    age: 25,
    isLogin: true
  },
// 主题设置
  theme: 'light',
// 修改主题的方法
  setTheme: (newTheme) =>set({ theme: newTheme }),
// 修改用户年龄的方法
  setUserAge: (age) =>set((state) => ({ user: { ...state.user, age } }))
}));
如果组件直接订阅整个状态对象,即使只用到其中一个字段,当状态中任何字段变化时,组件都会重新渲染:
// 组件 A:只需要显示用户名
function UserName() {
  // 订阅了整个状态对象(错误示范)
  const { user } = useAppStore(); 
  console.log('UserName 组件渲染了');

  return <div>用户名:{user.name}</div>;
}

// 组件 B:只需要显示主题
function ThemeDisplay() {
  // 同样订阅了整个状态对象(
  const { theme } = useAppStore(); 
  console.log('ThemeDisplay 组件渲染了');

  return <div>当前主题:{theme}</div>;
}
当调用 setUserAge(26) 修改用户年龄时,user 对象变化 → 即使 ThemeDisplay 只用到 theme(未变化),也会被强制重新渲染,控制台会打印 ThemeDisplay 组件渲染了。

那我们怎样解决这个问题? Selector 精确订阅!
通过 Selector 函数指定组件需要的具体字段,组件只会在 该字段变化时 重新渲染:
// 组件 A:只订阅 user.name
function UserName() {
  // 精确选择需要的字段
  const userName = useAppStore((state) => state.user.name); 
  console.log('UserName 组件渲染了');

  return <div>用户名:{userName}</div>;
}

// 组件 B:只订阅 theme
function ThemeDisplay() {
  // 精确选择需要的字段
  const theme = useAppStore((state) => state.theme); 
  console.log('ThemeDisplay 组件渲染了');

  return <div>当前主题:{theme}</div>;
}
当调用 setUserAge(26) 时,只有 user.age 变化,user.name 未变 → UserName 组件不会重新渲染。

当调用 setTheme('dark') 时,theme 变化 → 只有 ThemeDisplay 组件重新渲染,UserName 不受影响。


状态持久化:自动保存到 localStorage
Zustand 内置了 persist 中间件,可以一行代码让你的状态写入本地存储:
import { persist } from 'zustand/middleware'
const useUserStore = create(
  persist(
    (set) => ({
      token: '',
      setToken: (val) => set({ token: val }),
    }),
    {
      name: 'user-storage', // localStorage key
    }
  )
)
下次页面加载会自动恢复状态,无需你手动做缓存逻辑。

DevTools 支持
开发时我们想配合 Redux DevTools 面板使用,也只需要包一层:
import { devtools } from 'zustand/middleware'
const useStore = create(devtools((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 }))
})))
开起来直接就能调状态了,兼容性也很好。

⚠️ 使用 Zustand 的注意事项
Zustand 写法清爽,但也别太随性,以下几点建议别踩坑:
1.避免在组件外直接 getState() ,容易拿到过期值,优先使用 hooks 获取。
2.状态不可变更新需自己管理,Zustand 默认不帮你处理 immutable。

3.模块化拆分 store,保持单一职责,一个功能一个 store 文件,利于维护和团队协作。


🧩 拆分 Store:一个模块一个文件
在中大型项目中,推荐将不同的业务状态拆成独立模块,每个模块单独写一个 Store 文件,结构更清晰、维护更方便。
比如我们可以这样拆:
stores/tabStore.ts:管理多页签状态
stores/userStore.ts:管理用户登录态
stores/formStore.ts:表单缓存数据
stores/themeStore.ts:主题设置
每个 Store 内部维护自己的状态结构和操作函数,只暴露对应的 useXXXStore hook。
// stores/userStore.ts
interface UserState {
  token: string
  login: (token: string) => void
  logout: () => void
}

export const useUserStore = create<UserState>((set) => ({
  token: '',
  login: (token) => set({ token }),
  logout: () => set({ token: '' }),
}))
其他组件中直接使用:
const { token, login } = useUserStore()
这样做的好处是:
1.状态职责单一,易于维护
2.避免一个大 Store 里塞满所有业务逻辑

3.对新成员更友好,知道状态在哪改、在哪用


✅ 总结
如果你觉得 Redux 模板冗余、useContext 又力不从心,那 Zustand 会是一个非常舒服的中间选择:
1.写法清爽,没有 ceremony
2.类型推导完整,几乎不用手动声明
3.不用 Provider、不用 reducer、不用手动 dispatch

4.还能支持持久化、选择订阅、调试工具,非常全面


对我来说,Zustand 就是那个 刚刚好 的选择:干净利落、类型清晰、可扩展性强,非常适合中小型项目的快速开发。


附上zustand官网地址:https://github.com/pmndrs/zustand

用户评论