zustand 入门
一、先看两段代码:Zustand vs 原生 setState
1. 原生 React 组件内 setState 实现 count
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
- 特点:
count是组件私有状态,只能在当前组件内修改 / 使用;如果其他组件需要用count,只能通过 props 透传。
2. Zustand 实现 count(官网简化版)
import { create } from "zustand";
// 1. 创建全局store(可在任意组件导入使用)
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })), // 批量更新、依赖旧状态
}));
// 2. 组件A使用count
function CounterA() {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
return (
<div>
<span>组件A:{count}</span>
<button onClick={increment}>+1</button>
</div>
);
}
// 3. 组件B(无关组件)也能使用同一个count
function CounterB() {
const count = useCountStore((state) => state.count); // 共享同一份状态
return <div>组件B:{count}</div>;
}
- 特点:
count是全局共享状态,CounterA、CounterB 甚至项目任意组件,都能直接读取 / 修改同一份 count,无需 props 透传。 - Zustand 的
store是一个全局单一数据源,组件 A 和组件 B 读取的count都指向这个容器里的同一个值,实时跟踪变化,实现跨组件状态同步。
3. 原生 React 组件内使用多个状态
jsx
import { useState } from "react";
// 子组件A:只显示name
function CounterA({ name }) {
console.log("🔴 原生CounterA重渲染了"); // 重渲染日志
return <div>CounterA:{name}</div>;
}
// 子组件B:只显示count
function CounterB({ count }) {
console.log("🔴 原生CounterB重渲染了"); // 重渲染日志
return <div>CounterB:{count}</div>;
}
// 父组件:管理name和count,渲染A、B
function Parent() {
const [name, setName] = useState("张三");
const [count, setCount] = useState(0);
return (
<div>
<CounterA name={name} />
<CounterB count={count} />
<button onClick={() => setName("李四")}>原生:改name</button>
</div>
);
}
export default Parent;
运行结果(点击「改 name」按钮后)
控制台会打印两行日志:
🔴 原生CounterA重渲染了
🔴 原生CounterB重渲染了
- 原因:原生 React 中,父组件的任意 state 变化,会导致整个父组件重新渲染,所有子组件也会跟着重新渲染—— 哪怕子组件 B 只接收 count,和 name 无关,也会被强制重渲染(这就是原生的冗余重渲染问题)。
4. zustand 组件内多个状态精准订阅
先明确页面结构(原生和 Zustand 完全一致)
页面外层是一个父组件,里面包含 3 个东西:
-
<CounterA />:只显示「name」(不关心 count) -
<CounterB />:只显示「count」(不关心 name) -
一个按钮:要么改 name,要么改 count
import { create } from "zustand";
// 1. 创建全局store(管理name和count,和父组件无关)
const useStore = create((set) => ({
name: "张三",
count: 0,
setName: (newName) => set({ name: newName }),
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 子组件A:只读取store中的name(不关心count)
function CounterA() {
console.log("🟢 Zustand CounterA重渲染了"); // 重渲染日志
const name = useStore((state) => state.name); // 只订阅name
return <div>CounterA:{name}</div>;
}
// 子组件B:只读取store中的count(不关心name)
function CounterB() {
console.log("🟢 Zustand CounterB重渲染了"); // 重渲染日志
const count = useStore((state) => state.count); // 只订阅count
return <div>CounterB:{count}</div>;
}
// 父组件:只渲染A、B和按钮,不管理状态
function Parent() {
const increment = useStore((state) => state.increment); // 读取修改count的方法
return (
<div>
<CounterA /> {/* 不用传props,内部自己读store */}
<CounterB /> {/* 不用传props,内部自己读store */}
<button onClick={increment}>Zustand:count++</button>
</div>
);
}
export default Parent;
运行结果(点击「count++」按钮后)
控制台只会打印一行日志:
🟢 Zustand CounterB重渲染了
-
原因:Zustand 的核心是「组件独立订阅自己需要的状态」:
- CounterA 只订阅了 store 中的
name,没有订阅count—— 所以count变化时,CounterA 完全感知不到,不会重渲染; - CounterB 只订阅了 store 中的
count——count变化时,只有 CounterB 会重渲染; - 父组件没有管理任何 state,所以父组件也不会重渲染(自然不会强制子组件渲染)。
- CounterA 只订阅了 store 中的
二、zustand的核心优势
和原生的setState相比:
-
可维护性提升:全局状态管理,解决 “状态共享” 问题,避免多组件间通过透传等复杂方式传递共享的状态。所有修改状态的逻辑都集中在 store 里,组件只需要调用方法,无需关心内部实现。
-
重渲染性能提升:让组件只订阅自己需要的状态,只有订阅的状态变了,组件才重渲染。
-
天然支持异步状态管理:如果状态的更新依赖异步请求(比如从接口获取初始 count),原生 setState 需要在组件内写 useEffect,而 Zustand 可直接在 store 内封装异步函数。
和其他全局状态管理方案相比,官方提供了详细说明zustand.docs.pmnd.rs/getting-sta…
三、基础使用方法
定义 Store:包含状态和修改状态的方法
import { create } from 'zustand'
const useBear = create((set) => ({
// 状态
bears: 0,
// 方法
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}))
在组件中使用:订阅状态 + 调用方法
function BearCounter() {
const bears = useBear((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useBear((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
四、高级特性
(1)异步状态管理(官方原生支持,无需额外依赖)
const useUserStore = create((set) => ({
user: null,
isLoading: false,
// 异步方法:直接在 Store 内封装请求逻辑
fetchUser: async (userId) => {
set({ isLoading: true });
const res = await fetch(`/api/user/${userId}`);
const data = await res.json();
set({ user: data, isLoading: false }); // 请求完成更新状态
},
}));
// 组件中使用
function UserProfile({ userId }) {
const { user, isLoading, fetchUser } = useUserStore((state) => ({
user: state.user,
isLoading: state.isLoading,
fetchUser: state.fetchUser,
}));
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
if (isLoading) return <div>Loading...</div>;
return <div>Name: {user?.name}</div>;
}
(2)状态持久化(官方推荐 persist 中间件)
import { create } from "zustand";
import { persist } from "zustand/middleware"; // 导入官方中间件
// 用 persist 包裹 Store,自动持久化到 localStorage(默认)
const useCartStore = create(
persist(
(set) => ({
cartItems: [],
addToCart: (item) => set((state) => ({ cartItems: [...state.cartItems, item] })),
}),
{
name: "cart-storage", // 本地存储的 key(默认是 "zustand")
// 可选:自定义存储方式(如 sessionStorage)
// storage: sessionStorage,
}
)
);
(3)精准订阅优化(官方性能核心)
- 订阅多个状态(用
shallow避免引用类型误判):
import { shallow } from "zustand/shallow";
// 订阅多个状态,只有状态值真的变化才重渲染(适合对象/数组)
const { bears, user } = useBearStore(
(state) => ({ bears: state.bears, user: state.user }),
shallow // 浅比较:对比对象/数组的引用,避免不必要重渲染
);
- 选择性订阅(组件只关心部分状态):
// 只订阅 bears 的偶数状态(官方示例,灵活筛选)
const evenBears = useBearStore((state) => state.bears % 2 === 0);
(4)拆分 Store(官方推荐:按业务模块拆分)
// store/userStore.js
export const useUserStore = create((set) => ({/* 用户相关状态 */}));
// store/cartStore.js
export const useCartStore = create((set) => ({/* 购物车相关状态 */}));
// 组件中按需导入
import { useUserStore } from "./store/userStore";
import { useCartStore } from "./store/cartStore";
3. 调试工具(官方支持 Redux DevTools)
import { create } from "zustand";
import { devtools } from "zustand/middleware"; // 导入调试中间件
// 用 devtools 包裹,支持 Redux DevTools 查看状态变更记录
const useBearStore = create(
devtools((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}))
);
五、官方强调的注意事项
1. 状态订阅相关
-
🔴 避免订阅整个 Store(导致不必要重渲染):错误写法:
const store = useBearStore();(订阅所有状态,任意状态变化都触发重渲染)正确写法:const bears = useBearStore((state) => state.bears);(精准订阅单个状态) -
🟡 引用类型(对象 / 数组)的订阅:若订阅对象 / 数组,默认浅比较引用地址(如
{a:1}和{a:1}是不同引用,会触发重渲染),需手动用shallow或deepEqual优化:import { deepEqual } from "zustand/shallow"; const user = useBearStore((state) => state.user, deepEqual); // 深比较值
2. 状态更新相关
-
🔴 依赖旧状态必须用函数参数:错误写法:
set({ bears: bears + 1 })(可能获取到旧状态,因闭包问题)正确写法:set((state) => ({ bears: state.bears + 1 }))(官方推荐,确保拿到最新状态) -
🟡 批量更新:多次
set会自动批量合并(官方优化),无需手动处理:const updateUser = () => { set({ name: "李四" }); set({ age: 20 }); // 会和上一句合并为一次更新,避免多次重渲染 };
3. 组件卸载相关
-
🔴 异步请求未完成时组件卸载:若组件卸载后异步请求才返回,更新状态会导致警告,官方推荐用
abortController或状态判断:const fetchUser = async (userId) => { set({ isLoading: true }); const controller = new AbortController(); try { const res = await fetch(`/api/user/${userId}`, { signal: controller.signal }); const data = await res.json(); set((state) => ({ user: data, isLoading: false })); // 用函数参数确保状态最新 } catch (err) { if (err.name !== "AbortError") console.error(err); } return () => controller.abort(); // 组件卸载时取消请求 };
4. 性能优化相关
- 🟡 避免在订阅函数中创建新对象:错误写法:
useBearStore((state) => ({ bears: state.bears }))(每次返回新对象,触发重渲染)正确写法:useBearStore((state) => state.bears)或配合shallow使用 - 🟡 大型项目拆分细粒度 Store:官方不推荐创建 “单一大 Store”,而是按业务模块拆分(如用户、购物车、全局设置各自一个 Store),减少状态冗余和重渲染范围。
5. 其他官方提醒
- 无需 Provider 包裹:Zustand 不依赖 React Context,无需在根组件套 Provider(和 Redux/Pinia 不同);
- 支持非 React 环境:Store 可在非组件文件(如工具函数、API 请求文件)中直接使用
useBearStore.getState()读取状态、useBearStore.setState()修改状态; - 类型安全(TS 支持):官方原生支持 TypeScript,状态和方法自动推导类型,无需额外定义接口(推荐使用 TS 提升开发体验)。