Zustand

6 阅读8分钟

Zustand 基于 发布订阅(Pub/Sub)模式, 可有效规避 Context 性能问题。

基本理论

  • 事件总线: Zustand 通过 create 创建 Store (事件总线),多次调用 create 即可创建多条隔离的总线,总线间状态更新互不影响
  • 订阅总线: 组件中通过调用 useStore(如 useUserStore(selector))就是订阅指定的总线
  • 发布事件: Store 中调用 set 方法更新状态,就是向该总线的所有订阅者 “发布事件(状态更新)”
// 创建两条独立的“事件总线”(Store)
const useUserStore = create((set) => ({ userInfo: { name: '张三' } })); // 总线1:用户相关
const useCartStore = create((set) => ({ cart: [{ id: 1 }] })); // 总线2:购物车相关// 组件1:订阅“总线1”
const UserName = () => {
  const name = useUserStore(state => state.userInfo.name); // 只订阅总线1的name
  return <div>{name}</div>;
};
​
// 组件2:订阅“总线2”
const CartList = () => {
  const cart = useCartStore(state => state.cart); // 只订阅总线2的cart
  return <div>{cart.length}</div>;
};

基本使用

安装

npm install zustand

创建 Store

Zustand 使用 create 函数创建 store,它接受一个函数作为参数,该函数返回状态和更新函数。

// 1. 导入 Zustand 核心创建方法:create 是构建状态仓库(Store)的入口 API
//    语法格式:import { create } from 'zustand'
import { create } from 'zustand'// 2. 创建全局状态仓库(Store)
//    核心语法:create((set) => ({ 状态字段, 状态修改方法 }))
//    - set:Zustand 内置的状态更新函数,用于修改 Store 中的状态
//    - 返回值:自定义的 Hook(useStore),供组件消费状态
const useStore = create((set) => ({
  // 定义基础状态:count 初始值为 0(支持任意类型:数字、对象、数组等)
  count: 0,
​
  // 定义状态修改方法:递增 count
  // 语法:函数式更新(推荐)—— set 接收回调函数,参数 state 是当前最新状态
  // 适用场景:新状态依赖旧状态(避免状态竞态问题)
  increment: () => set((state) => ({ count: state.count + 1 })),
​
  // 定义状态修改方法:递减 count
  // 语法:同上,函数式更新,基于旧 state.count 计算新值
  decrement: () => set((state) => ({ count: state.count - 1 })),
​
  // 定义状态修改方法:重置 count 为 0
  // 语法:直接赋值更新 —— set 接收新状态对象
  // 适用场景:新状态不依赖旧状态,直接覆盖
  reset: () => set({ count: 0 }),
}))
​
// 3. React 组件中消费 Store 状态/方法
function Counter() {
  // 语法:状态选择器 —— useStore(selector 函数) 订阅指定状态
  // selector 函数参数:state 是 Store 完整状态对象,返回需要的单个状态字段
  // 特性:仅当 count 变化时,Counter 组件才会重渲染(精准订阅,性能最优)
  const count = useStore((state) => state.count)
​
  // 语法:方法选择器 —— 订阅 Store 中的方法(方法是静态函数,引用永不变化)
  // 特性:订阅方法不会触发组件重渲染(因为方法引用不变)
  const increment = useStore((state) => state.increment)
  
  // 同上,订阅 decrement 方法
  const decrement = useStore((state) => state.decrement)
  
  return (
    <div>
      {/* 语法:绑定方法到点击事件 —— 调用 decrement 方法修改 count 状态 */}
      <button onClick={decrement}>-</button>
      {/* 语法:渲染订阅的 count 状态 —— 状态更新时自动重新渲染该节点 */}
      <span>{count}</span>
      {/* 语法:绑定方法到点击事件 —— 调用 increment 方法修改 count 状态 */}
      <button onClick={increment}>+</button>
    </div>
  )
}
​
// 可选:导出组件,供其他模块引入使用
export default Counter;

数据流

Zustandy与Context对比

举一个购物车场景的例子,需要管理用户信息(userInfo) 和 购物车(cart)两个状态

Context方案

import { createContext, useContext, useState } from 'react';
​
// 1. 创建单一 Context,包含所有状态
const AppContext = createContext(null);
​
// 2. Context Provider 组件
export const AppProvider = ({ children }) => {
  // 状态:用户信息 + 购物车(单一对象)
  const [state, setState] = useState({
    userInfo: { name: '张三', age: 25 },
    cart: [{ id: 1, name: '商品A', count: 1 }]
  });
​
  // 修改购物车数量(仅修改 cart 子属性)
  const updateCartCount = (id, count) => {
    setState(prev => ({
      ...prev,
      cart: prev.cart.map(item => 
        item.id === id ? { ...item, count } : item
      )
    }));
  };
​
  // 修改用户名(仅修改 userInfo 子属性)
  const updateUserName = (name) => {
    setState(prev => ({
      ...prev,
      userInfo: { ...prev.userInfo, name }
    }));
  };
​
  return (
    <AppContext.Provider value={{ state, updateCartCount, updateUserName }}>
      {children}
    </AppContext.Provider>
  );
};
​
// 3. 消费组件1:仅使用 userInfo.name
const UserName = () => {
  const { state } = useContext(AppContext);
  console.log('UserName 组件重渲染了'); // 关键:观察重渲染
  return <div>用户名:{state.userInfo.name}</div>;
};
​
// 4. 消费组件2:仅使用 cart 列表
const CartList = () => {
  const { state, updateCartCount } = useContext(AppContext);
  console.log('CartList 组件重渲染了'); // 关键:观察重渲染
  return (
    <div>
      <h3>购物车</h3>
      {state.cart.map(item => (
        <div key={item.id}>
          {item.name} × {item.count}
          <button onClick={() => updateCartCount(item.id, item.count + 1)}>+1</button>
        </div>
      ))}
    </div>
  );
};
​
// 5. 根组件使用
const App = () => {
  return (
    <AppProvider>
      <UserName />
      <CartList />
    </AppProvider>
  );
};
问题:

粒度问题:Context 是「单一状态对象」,哪怕只改 cart,消费 userInfo.name 的 UserName 组件也会强制重渲染(因为 Context 的 value 引用变了); 重渲染不可控:无法精准控制仅更新用到 cart 的组件。

接下来尝试优化粒度

// 1. 拆分用户 Context 和购物车 Context
const UserContext = createContext(null);
const CartContext = createContext(null);
​
// 2. 多层 Provider 嵌套
export const AppProvider = ({ children }) => {
  const [userInfo, setUserInfo] = useState({ name: '张三', age: 25 });
  const [cart, setCart] = useState([{ id: 1, name: '商品A', count: 1 }]);
​
  const updateCartCount = (id, count) => {
    setCart(prev => prev.map(item => item.id === id ? { ...item, count } : item));
  };
  const updateUserName = (name) => {
    setUserInfo(prev => ({ ...prev, name }));
  };
​
  return (
    <UserContext.Provider value={{ userInfo, updateUserName }}>
      <CartContext.Provider value={{ cart, updateCartCount }}>
        {children}
      </CartContext.Provider>
    </UserContext.Provider>
  );
};
​
// 3. 消费组件
const UserName = () => {
  const { userInfo } = useContext(UserContext);
  console.log('UserName 组件重渲染了');
  return <div>用户名:{userInfo.name}</div>;
};
​
const CartList = () => {
  const { cart, updateCartCount } = useContext(CartContext);
  console.log('CartList 组件重渲染了');
  return (/* 购物车渲染逻辑 */);
};
新问题:

嵌套复杂度:每新增一个状态维度,就需要多一层 Provider 嵌套,大型应用中会出现「Provider 地狱」; 维护成本高:状态分散在多个 Context 中,跨状态逻辑(如「下单时更新用户信息 + 清空购物车」)需要同时操作多个 Context,代码冗余。

Zustand方案

import { create } from 'zustand';
​
// 1. 创建 Zustand Store(无 Provider,状态拆分清晰)
const useAppStore = create((set, get) => ({
  // 状态拆分:用户信息
  userInfo: { name: '张三', age: 25 },
  // 状态拆分:购物车
  cart: [{ id: 1, name: '商品A', count: 1 }],
​
  // 方法:修改用户名
  updateUserName: (name) => set(prev => ({
    userInfo: { ...prev.userInfo, name }
  })),
  // 方法:修改购物车数量
  updateCartCount: (id, count) => set(prev => ({
    cart: prev.cart.map(item => item.id === id ? { ...item, count } : item)
  })),
  // 跨状态逻辑(简洁)
  submitOrder: () => {
    const { cart, updateUserName } = get();
    console.log('下单:', cart);
    updateUserName('下单用户-' + Math.random());
    set({ cart: [] });
  }
}));
​
// 2. 消费组件:只订阅需要的状态
const UserName = () => {
  // 仅订阅 userInfo.name,其他状态变化不触发重渲染
  const userName = useAppStore(state => state.userInfo.name);
  console.log('UserName 组件重渲染了');
  return <div>用户名:{userName}</div>;
};
​
const CartList = () => {
  // 仅订阅 cart 和 updateCartCount,其他状态变化不触发重渲染
  const { cart, updateCartCount } = useAppStore(
    state => ({
      cart: state.cart,
      updateCartCount: state.updateCartCount
    }),
    // 可选:浅比较优化(避免对象引用变化导致的无意义重渲染)
    (a, b) => JSON.stringify(a.cart) === JSON.stringify(b.cart)
  );
  console.log('CartList 组件重渲染了');
  return (
    <div>
      <h3>购物车</h3>
      {cart.map(item => (
        <div key={item.id}>
          {item.name} × {item.count}
          <button onClick={() => updateCartCount(item.id, item.count + 1)}>+1</button>
        </div>
      ))}
      <button onClick={() => useAppStore.getState().submitOrder()}>下单</button>
    </div>
  );
};
​
// 3. 根组件:无需 Provider 嵌套
const App = () => {
  return (
    <div>
      <UserName />
      <CartList />
    </div>
  );
};
优势(Pub/Sub ):

粒度精准,重渲染可控:点击购物车「+1」按钮,仅 CartList 组件重渲染,UserName 组件无感知(因为它只订阅了 userInfo.name); 调用 submitOrder 时,UserName 会因 userInfo.name 变化重渲染,CartList 会因 cart 清空重渲染,完全符合预期。 无 Provider 嵌套:无需任何 Context.Provider,直接使用 useAppStore 即可消费状态,代码简洁,无「嵌套地狱」。 状态管理集中且灵活:状态和方法集中在一个 Store 中,跨状态逻辑(如下单)只需调用一个方法;组件可按需订阅任意状态片段(单个属性、多个属性、甚至派生状态)。

## Zustand原理

  1. 用「全局状态容器」存储所有状态(独立于 React 组件树);
  2. 用「发布 - 订阅系统」管理组件与状态的订阅关系;
  3. 用「选择器 + 对比逻辑」实现 “仅订阅状态变化时更新组件”。

基本类型(数字 / 字符串):默认 Object.is 对比; 对象 / 数组: Zustand 内置 shallow 浅比较;

import { create, shallow } from 'zustand';
​
const { cart, updateCartCount } = useAppStore(
  state => ({ cart: state.cart, updateCartCount: state.updateCartCount }),
  shallow // 浅比较:只对比 cart 数组的第一层属性
);

无 Provider 嵌套的底层原因

Context 依赖 React.createContext + Provider 透传状态,必须嵌套; Zustand 的 stateContainer 是全局独立的 JS 对象,不依赖 React 上下文

  • 组件通过 useAppStore Hook 直接读写全局状态容器;
  • 非 React 环境(如工具函数、API 回调)可通过 useAppStore.getState() 直接访问,无需依赖组件树:
// 非组件环境(如接口回调)更新状态
const handleOrderSuccess = () => {
  useAppStore.getState().submitOrder();
};

批量更新机制

Zustand 的 set 方法默认支持 “批量更新”:多次调用 set 只会触发一次订阅者检查

// 多次 set 但仅触发一次通知
const batchUpdate = () => {
  set({ cart: [] });
  set(prev => ({ userInfo: { ...prev.userInfo, name: '李四' } }));
};
补充
  • react函数组件卸载时Zustand自动移除订阅者(非react环境 需要手动subscribe、unsubscribe
(在 React 组件中使用 useStore(selector) 时,Zustand 内部通过 React 的 useEffect 钩子完成了「订阅注册 + 卸载自动取消」的逻辑)


//逻辑伪代码  
// Zustand 内部 useStore 简化逻辑
function useStore(selector) {
const [state, setState] = useState();
useEffect(() => {
// 1. 组件挂载时:订阅状态变化
const unsubscribe = store.subscribe((newState) => {
const selected = selector(newState);
setState(selected);
});
// 2. 组件卸载时:自动执行 unsubscribe 取消订阅
return () => unsubscribe();
}, [selector]);
return state;
}
  • 在异步更新中可用用get()确保拿到最新值,避免拿到过期状态
  • 常用中间件: // 1. persist:状态持久化(localStorage缓存)
  • // 2. devtools:Redux DevTools调试
import { create, persist, devtools } from 'zustand';

const useAppStore = create(
  devtools(
    persist(
      (set, get) => ({
        userInfo: { name: '张三', age: 25 },
        cart: [{ id: 1, name: '商品A', count: 1 }],
      }),
      {
        name: 'app-store-storage', // localStorage的key
        partialize: (state) => ({ cart: state.cart }) // 返回持久化的cart的对象形式
      }
    )
  )
);
partialize配置
配置项是否必填默认值作用说明
name无(必填项)存储数据的唯一 key,用于区分不同 Store 的持久化数据,避免存储冲突。
storagelocalStorage指定数据持久化的存储引擎,支持内置的 sessionStorage,也可传入自定义存储引擎(需符合 getItem/setItem/removeItem 接口规范)。
partialize无(默认持久化整个 state)筛选需要持久化的状态片段,接收 Store 完整状态 state 作为入参,必须以「对象形式」返回需要持久化的部分(无需持久化的状态会被忽略)。
merge默认浅合并 / 直接覆盖自定义页面刷新 / 重启后,持久化数据恢复到 Store 的逻辑,常用于处理「新旧状态合并」「深层属性保留」等场景。