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原理
- 用「全局状态容器」存储所有状态(独立于 React 组件树);
- 用「发布 - 订阅系统」管理组件与状态的订阅关系;
- 用「选择器 + 对比逻辑」实现 “仅订阅状态变化时更新组件”。
基本类型(数字 / 字符串):默认 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 的持久化数据,避免存储冲突。 |
storage | 否 | localStorage | 指定数据持久化的存储引擎,支持内置的 sessionStorage,也可传入自定义存储引擎(需符合 getItem/setItem/removeItem 接口规范)。 |
partialize | 否 | 无(默认持久化整个 state) | 筛选需要持久化的状态片段,接收 Store 完整状态 state 作为入参,必须以「对象形式」返回需要持久化的部分(无需持久化的状态会被忽略)。 |
merge | 否 | 默认浅合并 / 直接覆盖 | 自定义页面刷新 / 重启后,持久化数据恢复到 Store 的逻辑,常用于处理「新旧状态合并」「深层属性保留」等场景。 |