😎 用了 Zustand 后,我的状态逻辑精简了一半

515 阅读6分钟

状态管理一直是前端开发绕不开的话题。

用过 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
}
​
// 整个 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'export default function 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()

这样做的好处是:

  • 状态职责单一,易于维护
  • 避免一个大 Store 里塞满所有业务逻辑
  • 对新成员更友好,知道状态在哪改、在哪用

✅ 总结

如果你觉得 Redux 模板冗余、useContext 又力不从心,那 Zustand 会是一个非常舒服的中间选择:

  • 写法清爽,没有 ceremony
  • 类型推导完整,几乎不用手动声明
  • 不用 Provider、不用 reducer、不用手动 dispatch
  • 还能支持持久化、选择订阅、调试工具,非常全面

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

如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看!

后续我会持续更新 React 重学系列,👉 记得关注我,不错过每一篇实战干货!