目 录CONTENT

文章目录

React(十八)Zustand

React 状态管理:Zustand 完全指南

本教程面向 React 初学者与前端进阶开发者,用生活化的语言讲解 Zustand 的核心概念、使用方法和常见踩坑点。所有代码示例均可直接运行。


一、Zustand 是什么?

1.1 一句话定义

Zustand(德语:状态)是一个极简、零依赖的 React 状态管理库。

它允许你用很少的代码创建一个可全局访问的“状态仓库”(Store),并在任意 React 组件中读取和更新状态,不需要像 Redux 那样写模板代码,也不需要像 Context 那样包裹 Provider。

1.2 为什么要用 Zustand?先对比其他方案

方案 优点 缺点 适合场景
useState 简单、React 内置 只能本组件使用,跨组件传递麻烦 单个组件内部状态
Context 官方方案,可跨层级传递 任一值变化会导致所有消费者重渲染;Provider 嵌套地狱 主题、语言、用户身份等低频变化的全局数据
Redux 生态强大、可预测 样板代码多、学习曲线陡、需要写 action/reducer 超大型应用、复杂状态流转
MobX 响应式、代码少 语法有一定魔法感,调试需要适应 中大型应用
Zustand 极简 API、无 Provider、按需订阅、最小重渲染 生态比 Redux 小(但常用插件齐全) 小到中大型 React 应用

生活中的类比

  • useState 就像你的手机备忘录,只能自己看、自己改。
  • Context 像班级公告栏,但是一更新公告,全班所有人都要站起来看一遍,哪怕内容和自己没关系。
  • Redux 像一套完整的办公流程:写申请表、找领导签字、由专人修改档案。流程规范,但琐事多。
  • Zustand 像公司里的共享文件柜:每个员工拿一把钥匙,谁要拿什么文件自己取,只会在自己需要的文件更新时收到通知。

1.3 Zustand 的核心优势

  1. 极简 API:创建 Store 只需一行 create(),就能同时得到状态和方法。
  2. 最小重渲染:组件只订阅自己需要的状态片段,其他状态变化不会触发重渲染。
  3. 跨组件 / 无组件访问:Store 是一个普通对象,不仅组件能用,也可以在工具函数、事件回调中调用。
  4. 无 Provider 嵌套:不需要在 App 根节点包裹 Provider,组件树保持干净。
  5. TypeScript 友好:天然类型推导,无需额外配置。

1.4 底层运行原理说明

Zustand 的运行可以简化成三个步骤:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   创建 Store     │ ──▶ │   组件订阅状态   │ ──▶ │  状态更新通知    │
│  create() 函数  │     │  useXxxStore()  │     │  订阅器重新渲染  │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Store 内部大致结构

  • 一个保存当前状态的 JavaScript 对象。
  • 一个 set 函数,用来更新状态(并通知订阅者)。
  • 一个 get 函数,用来读取当前状态。
  • 一个订阅者列表,记录哪些组件正在监听哪些状态。

当调用 set 更新状态时,Zustand 会检查每个订阅者关心的状态是否发生变化。如果变化了,就触发该组件重渲染;否则跳过。这保证了只有“相关”的组件才会更新,而不是全局一起刷新。


二、Zustand 完整使用流程

2.1 环境搭建:安装 Zustand

Zustand 支持 npm、yarn、pnpm 三种常见包管理器:

# npm
npm install zustand

# yarn
yarn add zustand

# pnpm
pnpm add zustand

同时确保你的 React 项目已创建(例如使用 Vite):

npm create vite@latest zustand-demo -- --template react

如果你使用 TypeScript,选择 react-ts 模板即可,Zustand 对 TS 支持非常友好。

2.2 基础 Store:计数器

在项目中新建文件 store.js(或 store.ts):

// store.js
import { create } from "zustand";

export const useCounterStore = create((set) => ({
  // 1. 状态
  count: 0,

  // 2. 修改状态的方法
  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

API 拆解

代码 含义
create() 创建 Store 的核心函数,接收一个函数作为参数。
(set) => ({ ... }) 参数函数接收 setget,返回一个对象,对象里的属性就是状态和方法。
set(...) 更新状态。可以直接传新对象,也可以传函数基于旧状态更新。
useCounterStore 自定义 Hook,可以在组件中调用它来读取状态。

为什么 set 推荐用函数?

因为 set((state) => ({ count: state.count + 1 })) 是基于当前最新状态更新的,可以避免并发更新时的覆盖问题。就像排队买票时,售票员根据当前余票数量再决定卖不卖,而不是直接按你最初看到的数量处理。

2.3 在组件中使用 Store

场景 A:读取状态并显示

// Display.jsx
import { useCounterStore } from "./store";

function Display() {
  const count = useCounterStore((state) => state.count);

  return <h1>当前计数:{count}</h1>;
}

export default Display;

useCounterStore((state) => state.count) 的意思是:我只关心 count 这个字段。只有当 count 变化时,这个组件才会重新渲染。

场景 B:调用 Store 方法

// Controls.jsx
import { useCounterStore } from "./store";

function Controls() {
  // 同时读取多个方法,可以一次解构
  const { increase, decrease, reset } = useCounterStore((state) => ({
    increase: state.increase,
    decrease: state.decrease,
    reset: state.reset,
  }));

  return (
    <div>
      <button onClick={increase}>+1</button>
      <button onClick={decrease}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

export default Controls;

场景 C:合并展示

// App.jsx
import Display from "./Display";
import Controls from "./Controls";

function App() {
  return (
    <div style={{ padding: 20 }}>
      <Display />
      <Controls />
    </div>
  );
}

export default App;

到这里,一个最基础的 Zustand 应用就搭建完成了。它比 useState 多一步创建 Store,但换来的是跨组件自由共享状态的能力。


三、Zustand 进阶用法

3.1 分片读取状态:优化重渲染

Zustand 最强大的特性之一就是精细化订阅

// Bad:直接解构整个 Store
function UserCard() {
  const { name, age, city } = useUserStore(); // 任一状态变化都会触发重渲染
  return (
    <div>
      <p>{name}</p>
      <p>{age}</p>
    </div>
  );
}
// Good:只订阅需要的字段
function UserCard() {
  const name = useUserStore((state) => state.name);
  const age = useUserStore((state) => state.age);
  return (
    <div>
      <p>{name}</p>
      <p>{age}</p>
    </div>
  );
}

业务价值:在表单、列表、仪表盘等场景中,避免一个字段更新导致整个页面重绘,大幅提升性能。

3.2 中间件

Zustand 的中间件可以扩展 Store 能力。下面介绍三个最常用的。

3.2.1 persist:状态持久化

场景:用户登录 token、主题设置、购物车数据需要在刷新页面后保留。

import { create } from "zustand";
import { persist } from "zustand/middleware";

export const useThemeStore = create(
  persist(
    (set) => ({
      theme: "light",
      toggleTheme: () =>
        set((state) => ({
          theme: state.theme === "light" ? "dark" : "light",
        })),
    }),
    {
      name: "theme-storage", // localStorage 的 key
    },
  ),
);

这样,theme 会自动同步到 localStorage,刷新页面后仍然保留。

3.2.2 devtools:Redux DevTools 调试

场景:开发阶段追踪状态变化,排查 bug。

import { create } from "zustand";
import { devtools } from "zustand/middleware";

export const useCounterStore = create(
  devtools(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 })),
    }),
    { name: "CounterStore" },
  ),
);

安装浏览器 Redux DevTools 扩展后,即可在面板中查看每个状态变更。

3.2.3 immer:像修改普通对象一样更新状态

场景:嵌套对象或数组更新时,不用手动展开所有层级。

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

export const useCartStore = create(
  immer((set) => ({
    items: [],
    addItem: (product) =>
      set((state) => {
        state.items.push(product); // 直接修改,immer 会自动生成不可变快照
      }),
    removeItem: (id) =>
      set((state) => {
        state.items = state.items.filter((item) => item.id !== id);
      }),
  })),
);

业务价值:在处理表单、嵌套配置、购物车等复杂数据结构时,immer 让代码可读性接近普通对象操作,同时保持 React 所需的不可变更新语义。

3.3 Store 的拆分与组合

大型项目不要写成一个万能 Store,而是按业务模块拆分,再组合使用。

// userStore.js
import { create } from "zustand";

export const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
}));
// cartStore.js
import { create } from "zustand";

export const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clear: () => set({ items: [] }),
}));
// App.jsx 中按需组合使用
import { useUserStore } from "./userStore";
import { useCartStore } from "./cartStore";

function Header() {
  const user = useUserStore((state) => state.user);
  const count = useCartStore((state) => state.items.length);

  return (
    <header>
      <span>欢迎,{user?.name ?? "游客"}</span>
      <span>购物车({count})</span>
    </header>
  );
}

业务价值:每个 Store 职责单一,便于维护、测试和团队协作。

3.4 在 React 组件外调用 Store

Zustand 的 Store 是一个普通对象,可以直接调用方法,这在事件处理、工具函数、非 React 代码中非常有用。

import { useCounterStore } from "./store";

// 普通函数中直接调用
function someUtility() {
  useCounterStore.getState().increase();
  console.log(useCounterStore.getState().count);
}

// 在 API 请求拦截器中重置登录状态
apiClient.interceptors.response.use(
  (res) => res,
  (error) => {
    if (error.response?.status === 401) {
      useUserStore.getState().logout();
    }
    return Promise.reject(error);
  },
);

业务价值:在路由守卫、API 拦截器、定时任务、原生 DOM 事件等场景中,无需把 Store 注入组件也能操作状态。


四、开发注意事项与常见踩坑

4.1 直接解构 Store 导致全量重渲染

错误写法

function Profile() {
  const { name, age, city } = useUserStore(); // 订阅了全部状态
  return <p>{name}</p>;
}

问题:只要 agecity 变化,即使 Profile 只显示 name,组件也会重新渲染。

正确写法

function Profile() {
  const name = useUserStore((state) => state.name);
  return <p>{name}</p>;
}

4.2 异步更新状态的常见错误

错误写法

const useUserStore = create((set) => ({
  user: null,
  fetchUser: async () => {
    const res = await fetch("/api/user");
    const user = await res.json();
    set({ user }); // 正确
  },
}));

上面的写法本身没问题,但初学者容易犯的错误是:

// 错误:在 async 函数里直接 return 数据,没有 set
fetchUser: async () => {
  const res = await fetch("/api/user");
  return await res.json(); // 只是返回了数据,状态没有更新!
};

正确写法:异步方法里必须调用 set 才能更新状态。

fetchUser: async () => {
  set({ user: null }); // 先清空,显示 loading
  const res = await fetch("/api/user");
  const user = await res.json();
  set({ user });
};

组件中:

const user = useUserStore((state) => state.user);
const fetchUser = useUserStore((state) => state.fetchUser);

if (!user) return <button onClick={fetchUser}>加载用户</button>;
return <p>{user.name}</p>;

4.3 持久化存储的版本兼容问题

错误场景:产品上线后,你发现 user 结构变了,但用户本地还保存着旧版数据。

const useUserStore = create(
  persist(
    (set) => ({
      user: { name: "", age: 0 }, // 新版字段变了
    }),
    { name: "user-storage" },
  ),
);

问题:旧版 localStorage 里可能存的是 { name: 'Tom' },新版读取后会缺少 age,甚至出现 undefined 错误。

正确写法:使用 versionmigrate 字段处理迁移。

const useUserStore = create(
  persist(
    (set) => ({
      user: { name: "", age: 0 },
    }),
    {
      name: "user-storage",
      version: 2, // 版本号升级
      migrate: (persistedState, version) => {
        if (version === 1) {
          persistedState.user.age = persistedState.user.age ?? 0;
        }
        return persistedState;
      },
    },
  ),
);

4.4 SSR 场景下的状态同步问题

错误场景:在 Next.js 等 SSR 框架中,服务端渲染的 HTML 和客户端 hydration 状态不一致,出现闪烁或报错。

原因:服务端没有 localStoragepersist 默认在服务端不会生效; hydration 时客户端状态可能和服务器 HTML 不一致。

正确写法:用 skipHydration 或自定义 storage 控制 hydration。

import { persist, createJSONStorage } from "zustand/middleware";

const useUserStore = create(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
    }),
    {
      name: "user-storage",
      storage: createJSONStorage(() => localStorage),
      skipHydration: true,
    },
  ),
);

同时在页面中手动触发 hydration:

useEffect(() => {
  useUserStore.persist.rehydrate();
}, []);

4.5 大型项目中的 Store 拆分规范

错误示范:一个 Store 包含所有业务状态。

const useAppStore = create((set) => ({
  user: null,
  cart: [],
  orders: [],
  settings: {},
  // ... 所有状态都塞在这里
}));

问题:文件臃肿、职责混乱、多人协作容易冲突、难以复用和测试。

正确规范

  1. 按业务模块拆分userStore.jscartStore.jsorderStore.js
  2. Store 命名统一useXxxStore,文件名为 xxxStore.js
  3. 状态和方法放在一起:与该模块相关的所有状态、异步请求、计算操作都放到同一个 Store。
  4. 跨 Store 通信优先使用方法调用:例如 orderStore 提交订单后清空购物车,可以直接调用 useCartStore.getState().clear()
// orderStore.js
import { create } from "zustand";
import { useCartStore } from "./cartStore";

export const useOrderStore = create((set) => ({
  orders: [],
  submitOrder: async () => {
    const items = useCartStore.getState().items;
    const res = await fetch("/api/order", {
      method: "POST",
      body: JSON.stringify({ items }),
    });
    const order = await res.json();
    set((state) => ({ orders: [...state.orders, order] }));
    useCartStore.getState().clear(); // 清空购物车
  },
}));

五、实战练习题

练习 1:基础计数器(入门)

目标:实现一个可以加减的计数器,并且限制最小值为 0,最大值为 10。

要求

  • 使用 Zustand 创建 Store。
  • 提供 increasedecreasereset 三个方法。
  • count 达到 0 时,decrease 无效;当达到 10 时,increase 无效。

练习 2:待办事项列表(进阶)

目标:实现一个 Todo List。

要求

  • Store 包含 todos 数组和 filter 字段。
  • 提供 addTodo(text)toggleTodo(id)removeTodo(id)setFilter(filter) 方法。
  • 组件中展示过滤后的列表(全部 / 已完成 / 未完成)。
  • 使用 persist 中间件持久化到 localStorage

练习 3:用户信息跨页面持久化(综合)

目标:模拟一个登录系统,用户信息在多个页面共享并持久化。

要求

  • 创建 useUserStore,包含 user{ name, role })和 login(name, role)logout() 方法。
  • 使用 persist 持久化,刷新页面后仍然保持登录状态。
  • 在登录页输入用户名和角色,登录后跳转到首页展示欢迎信息。
  • 提供退出登录按钮,点击后清空状态并返回登录页。

六、参考答案与思路解析

练习 1 参考答案

// store.js
import { create } from "zustand";

export const useCounterStore = create((set) => ({
  count: 0,
  increase: () =>
    set((state) => ({
      count: Math.min(state.count + 1, 10),
    })),
  decrease: () =>
    set((state) => ({
      count: Math.max(state.count - 1, 0),
    })),
  reset: () => set({ count: 0 }),
}));
// App.jsx
import { useCounterStore } from "./store";

function App() {
  const count = useCounterStore((state) => state.count);
  const { increase, decrease, reset } = useCounterStore((state) => ({
    increase: state.increase,
    decrease: state.decrease,
    reset: state.reset,
  }));

  return (
    <div style={{ padding: 20 }}>
      <h1>计数器:{count}</h1>
      <button onClick={increase}>+1</button>
      <button onClick={decrease}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

export default App;

思路:在 set 函数里用 Math.min / Math.max 限制范围,确保不会越界。


练习 2 参考答案

// store.js
import { create } from "zustand";
import { persist } from "zustand/middleware";

let nextId = 1;

export const useTodoStore = create(
  persist(
    (set) => ({
      todos: [],
      filter: "all", // all | completed | active

      addTodo: (text) =>
        set((state) => ({
          todos: [...state.todos, { id: nextId++, text, completed: false }],
        })),

      toggleTodo: (id) =>
        set((state) => ({
          todos: state.todos.map((todo) =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo,
          ),
        })),

      removeTodo: (id) =>
        set((state) => ({
          todos: state.todos.filter((todo) => todo.id !== id),
        })),

      setFilter: (filter) => set({ filter }),
    }),
    { name: "todo-storage" },
  ),
);
// App.jsx
import { useMemo } from "react";
import { useTodoStore } from "./store";

function App() {
  const todos = useTodoStore((state) => state.todos);
  const filter = useTodoStore((state) => state.filter);
  const { addTodo, toggleTodo, removeTodo, setFilter } = useTodoStore(
    (state) => ({
      addTodo: state.addTodo,
      toggleTodo: state.toggleTodo,
      removeTodo: state.removeTodo,
      setFilter: state.setFilter,
    }),
  );

  const filteredTodos = useMemo(() => {
    if (filter === "completed") return todos.filter((t) => t.completed);
    if (filter === "active") return todos.filter((t) => !t.completed);
    return todos;
  }, [todos, filter]);

  const handleAdd = (e) => {
    e.preventDefault();
    const text = e.target.text.value.trim();
    if (!text) return;
    addTodo(text);
    e.target.reset();
  };

  return (
    <div style={{ padding: 20 }}>
      <h1>待办事项</h1>
      <form onSubmit={handleAdd}>
        <input name="text" placeholder="输入事项" />
        <button type="submit">添加</button>
      </form>

      <div>
        <button onClick={() => setFilter("all")}>全部</button>
        <button onClick={() => setFilter("active")}>未完成</button>
        <button onClick={() => setFilter("completed")}>已完成</button>
      </div>

      <ul>
        {filteredTodos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

思路:用 filter 控制展示状态,用 useMemo 过滤列表避免重复计算。persist 让数据刷新不丢失。


练习 3 参考答案

// userStore.js
import { create } from "zustand";
import { persist } from "zustand/middleware";

export const useUserStore = create(
  persist(
    (set) => ({
      user: null,
      login: (name, role) => set({ user: { name, role } }),
      logout: () => set({ user: null }),
    }),
    { name: "user-storage" },
  ),
);
// Login.jsx
import { useUserStore } from "./userStore";

function Login({ onLogin }) {
  const login = useUserStore((state) => state.login);

  const handleSubmit = (e) => {
    e.preventDefault();
    const name = e.target.name.value;
    const role = e.target.role.value;
    login(name, role);
    onLogin();
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>登录</h2>
      <input name="name" placeholder="用户名" required />
      <select name="role" required>
        <option value="admin">管理员</option>
        <option value="user">普通用户</option>
      </select>
      <button type="submit">登录</button>
    </form>
  );
}

export default Login;
// Home.jsx
import { useUserStore } from "./userStore";

function Home({ onLogout }) {
  const user = useUserStore((state) => state.user);
  const logout = useUserStore((state) => state.logout);

  const handleLogout = () => {
    logout();
    onLogout();
  };

  return (
    <div>
      <h2>欢迎,{user.name}!</h2>
      <p>你的角色是:{user.role}</p>
      <button onClick={handleLogout}>退出登录</button>
    </div>
  );
}

export default Home;
// App.jsx
import { useState } from "react";
import Login from "./Login";
import Home from "./Home";
import { useUserStore } from "./userStore";

function App() {
  const user = useUserStore((state) => state.user);
  const [view, setView] = useState(user ? "home" : "login");

  return (
    <div style={{ padding: 20 }}>
      {view === "login" ? (
        <Login onLogin={() => setView("home")} />
      ) : (
        <Home onLogout={() => setView("login")} />
      )}
    </div>
  );
}

export default App;

思路:Store 负责“有没有用户”这个状态,组件只负责视图切换。persist 让登录状态在页面刷新后保留。实际项目中,view 状态可以用 React Router 路由替代,这里为了简化用 useState 控制。


总结

Zustand 是一个把“简单”做到极致的 React 状态管理库。它的核心思想是:

  • create 创建一个全局 Store。
  • 用选择器函数 (state) => state.xxx 精确订阅需要的状态。
  • 用中间件扩展持久化、调试、不可变更新等能力。
  • 按业务模块拆分 Store,保持项目清晰。

掌握这些要点后,你就已经能够在中大型 React 项目中熟练使用 Zustand 了。课后请完成三个练习题,并尝试把 persistimmer 组合使用,看看会有怎样的效果!

0
博主关闭了当前页面的评论