Skip to content

Zustand 深入

极简状态管理:基础使用、中间件体系、核心原理与源码解析

概述

Zustand(德语"状态")是由 Pmndrs(Poimandres)团队开发的轻量级状态管理库,核心代码仅约 40 行。它的设计哲学是:用最少的 API 覆盖最常见的状态管理需求


一、优势与劣势

优势

  • 极简 APIcreate 一个函数就完成 store 定义,无 Provider、无 Action Type、无 Reducer
  • 极小体积:gzip 后约 1KB,几乎零成本引入
  • 无 Provider:store 是模块级单例,不污染组件树,也可在 React 外使用
  • 选择器订阅:通过 selector 函数实现细粒度订阅,只有选中的值变化才触发重渲染
  • 中间件生态:persist(持久化)、devtools(调试)、immer(可变风格)、subscribeWithSelector 等
  • TypeScript 友好:类型推断开箱即用
  • 异步原生支持:action 中直接 async/await,无需额外中间件(对比 Redux Thunk/Saga)

劣势

  • 无内置派生状态:派生计算需要手动在 selector 或组件中实现(对比 Jotai 的 derived atom)
  • 单 store 模式:大型应用如果所有状态放一个 store,组织性不如 Redux Toolkit 的 slice 划分清晰
  • selector 引用陷阱:返回新对象/数组的 selector 需要配合 shallow 比较,否则每次都重渲染
  • 调试能力弱于 Redux:虽然支持 DevTools,但缺少 action 日志和时间旅行的完整体验

二、基础使用

安装

bash
npm install zustand

创建 Store

jsx
import { create } from 'zustand';

const useStore = create((set, get) => ({
  // 状态
  count: 0,
  user: null,
  todos: [],

  // 同步 action
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),

  // 异步 action
  fetchUser: async (id) => {
    const res = await fetch(`/api/users/${id}`);
    const user = await res.json();
    set({ user });
  },

  // 使用 get() 读取当前状态
  addTodo: (text) => {
    const todos = get().todos;
    set({ todos: [...todos, { id: Date.now(), text, done: false }] });
  },

  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ),
  })),
}));

在组件中使用

jsx
// 选择器:只订阅需要的状态片段
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  // count 变化时重渲染,todos 变化时不重渲染
  return <button onClick={increment}>{count}</button>;
}

function UserProfile() {
  const user = useStore((state) => state.user);
  const fetchUser = useStore((state) => state.fetchUser);

  useEffect(() => { fetchUser(1); }, [fetchUser]);

  if (!user) return <Skeleton />;
  return <div>{user.name}</div>;
}

function TodoList() {
  const todos = useStore((state) => state.todos);
  const toggleTodo = useStore((state) => state.toggleTodo);

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}
          style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Selector 与浅比较

jsx
import { useShallow } from 'zustand/react/shallow';

// ❌ 每次返回新对象 → 每次都重渲染
const { count, name } = useStore((state) => ({
  count: state.count,
  name: state.name,
}));

// ✅ useShallow 进行浅比较 → 只有值真正变化才重渲染
const { count, name } = useStore(
  useShallow((state) => ({ count: state.count, name: state.name }))
);

在 React 外使用

javascript
// 在非 React 代码中(如路由守卫、工具函数)
const state = useStore.getState();
console.log(state.count);

useStore.setState({ count: 99 });

// 订阅变化
const unsub = useStore.subscribe((state) => {
  console.log('状态变化:', state);
});

三、中间件

persist —— 持久化

jsx
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

const useStore = create(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'app-settings',                          // localStorage key
      storage: createJSONStorage(() => localStorage), // 默认 localStorage
      partialize: (state) => ({ theme: state.theme }), // 只持久化部分状态
      version: 1,                                      // 版本号(迁移用)
      migrate: (persisted, version) => {               // 版本迁移
        if (version === 0) {
          persisted.theme = persisted.theme || 'light';
        }
        return persisted;
      },
    }
  )
);

devtools —— Redux DevTools

jsx
import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      // 第三个参数给 action 命名(在 DevTools 中显示)
      increment: () => set(
        (state) => ({ count: state.count + 1 }),
        false,
        'increment'
      ),
    }),
    { name: 'CounterStore', enabled: process.env.NODE_ENV === 'development' }
  )
);

immer —— 可变风格更新

jsx
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer((set) => ({
    todos: [],
    addTodo: (text) => set((state) => {
      state.todos.push({ id: Date.now(), text, done: false }); // 直接 push
    }),
    toggleTodo: (id) => set((state) => {
      const todo = state.todos.find(t => t.id === id);
      if (todo) todo.done = !todo.done; // 直接修改
    }),
  }))
);

中间件组合

jsx
const useStore = create(
  devtools(
    persist(
      immer((set) => ({
        // ...状态和 action
      })),
      { name: 'my-storage' }
    ),
    { name: 'MyStore' }
  )
);
// 顺序:外层到内层 → devtools → persist → immer → 核心逻辑

四、模式与最佳实践

Slice 模式(大型应用拆分)

typescript
// slices/userSlice.ts
interface UserSlice {
  user: User | null;
  fetchUser: (id: string) => Promise<void>;
}

const createUserSlice = (set, get): UserSlice => ({
  user: null,
  fetchUser: async (id) => {
    const user = await fetchUserAPI(id);
    set({ user });
  },
});

// slices/todoSlice.ts
interface TodoSlice {
  todos: Todo[];
  addTodo: (text: string) => void;
}

const createTodoSlice = (set, get): TodoSlice => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, done: false }],
  })),
});

// store.ts — 合并 slice
const useStore = create<UserSlice & TodoSlice>()((...args) => ({
  ...createUserSlice(...args),
  ...createTodoSlice(...args),
}));

五、核心原理

源码解析(简化)

javascript
// Zustand 核心实现只有约 40 行
function createStore(createState) {
  let state;
  const listeners = new Set();

  const getState = () => state;

  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state = replace ? nextState : { ...state, ...nextState };
      listeners.forEach((listener) => listener(state, previousState));
    }
  };

  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };

  const api = { getState, setState, subscribe };
  state = createState(setState, getState, api);
  return api;
}

与 React 的连接

javascript
// create() 返回的 useStore Hook 基于 useSyncExternalStore
import { useSyncExternalStore } from 'react';

function useStore(selector, equalityFn = Object.is) {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
    () => selector(store.getServerState?.() ?? store.getState()) // SSR
  );
}

关键设计

  1. 发布-订阅模式subscribe 注册监听器,setState 触发通知
  2. useSyncExternalStore:React 18 官方 API,保证并发模式下的数据一致性(无 tearing)
  3. selector 决定重渲染:每次 state 变化后,React 用 selector 取值并比较,值没变则跳过渲染
  4. 闭包单例:store 存在模块作用域闭包中,不依赖 Context / Provider

面试高频题

Q: Zustand 为什么不需要 Provider?

Zustand 的 store 是模块级单例,状态存储在 createStore 的闭包中。组件通过 useSyncExternalStore 直接订阅这个外部 store,不需要 React Context 的传递链。好处是更简洁,而且可以在 React 组件外(如路由守卫、工具函数)直接读写状态。

Q: Zustand 的 selector 是怎么避免不必要重渲染的?

useSyncExternalStore 在 store 变化时,调用 selector 取新值,与上一次的值进行 Object.is 比较。如果相等则跳过重渲染。所以:

  • 选择原始值(state.count)天然稳定
  • 选择新对象需要配合 useShallow 或自定义 equalityFn

Q: Zustand 和 Redux 的核心区别?

  • API 复杂度:Zustand 无 action type / reducer / dispatch,一个 create 搞定
  • Provider:Redux 必须 Provider 包裹,Zustand 不需要
  • 异步:Zustand 直接 async/await,Redux 需要 thunk/saga 中间件
  • 体积:Zustand ~1KB vs Redux Toolkit ~11KB
  • 适合场景:Zustand 适合中小项目快速上手,Redux 适合大型团队严格规范