Zustand轻量状态管理框架
在前两章中我们介绍了Redux及其进化版Redux Toolkit(RTK),虽然RTK简化了许多Redux的繁琐操作,但Redux的整个体系仍然过于笨重,除非是数据流极端复杂的超大型项目,一般来说RTK可能都不是最好的选择。Zustand(德语“状态”的意思)是一个基于React Hooks的轻量级状态管理框架,相比RTK它有以下优势:
API设计人性化:Zustand的API采用了极简设计,不像Redux那样需要严格按照Reducer、Action、Dispatch等概念编写代码,心智负担极低。
无需Provider:Zustand的Store不需要用Provider组件包裹应用,任何组件都可以直接引入并使用。
基于React Hooks:Zustand完全基于React Hooks设计,与函数式组件天然契合。
代码精简:Zustand的核心代码规模不大,对构建打包非常友好,而且代码清晰值得学习。
安装Zustand
执行以下命令安装Zustand。
npm i zustand
Zustand的基本使用
创建Store
Zustand中创建Store非常简单,只需要调用create函数并传入一个回调函数即可。回调函数接收set参数,用于更新Store中的状态数据,函数返回值就是Store的初始状态和操作方法。
store/useCounterStore.js
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
export default useCounterStore;
上面代码中,我们通过create函数创建了一个名为useCounterStore的Store。注意create的返回值本身就是一个Hooks函数,因此我们按照Hooks的命名规范以use开头命名。Store中包含了一个状态数据count和三个操作方法increment、decrement、reset。
set函数用于更新状态数据,它支持两种用法:
- 传入一个对象,例如
set({ count: 0 }),直接设置状态值 - 传入一个函数,例如
set((state) => ({ count: state.count + 1 })),基于当前状态计算新状态
注意set函数默认执行的是浅合并(shallow merge),即只会合并顶层属性不会覆盖整个状态对象,如果要设置的某个属性是一个对象或数组,那么它就会被整体替换,这和React的useState行为一致。
补充说明:浅合并如何理解?浅合并是指set({ a: 1 })不会影响state.b,但如果state.user = { name: "Alice" },然后set({ user: { age: 25 } }),整个user对象会被替换,而不是合并。
在组件中使用Store
使用Zustand的Store非常直观,直接在组件中调用Store的Hooks函数即可,不需要任何Provider包裹。
App.jsx
import useCounterStore from "@/store/useCounterStore";
const App = () => {
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<div>{count}</div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
};
export default App;
对比Redux Toolkit的写法,Zustand的代码量明显减少了很多,我们不需要useSelector、useDispatch,也不需要Provider,直接从Store中解构出数据和方法就可以使用了。
Store部分订阅
上面代码中我们通过useCounterStore()获取了Store中的所有数据和方法,这意味着Store中任何数据发生变化都会触发组件的重新渲染。在实际开发中,如果一个Store中包含较多数据,而某个组件只关心其中一部分,我们可以仅部分订阅需要的数据,避免不必要的重新渲染。
App.jsx
import useCounterStore from "@/store/useCounterStore";
const DisplayComponent = () => {
const count = useCounterStore((state) => state.count);
return <div>{count}</div>;
};
const ControlComponent = () => {
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
const App = () => {
return (
<div>
<DisplayComponent />
<ControlComponent />
</div>
);
};
export default App;
上面代码中,DisplayComponent只订阅了count数据,而ControlComponent只订阅了操作方法。此时,当count发生变化时,只有DisplayComponent会重新渲染,而ControlComponent不会受到影响,避免了不必要的渲染开销。
此外,如果需要从Store中选取多个值,这可以通过返回对象的方式实现,但此时需要配合useShallow使用,否则每次都会返回一个新的对象引用从而导致重新渲染。
App.jsx
import { useShallow } from "zustand/react/shallow";
import useCounterStore from "@/store/useCounterStore";
const App = () => {
const { count, increment } = useCounterStore(
useShallow((state) => ({
count: state.count,
increment: state.increment,
})),
);
return (
<div>
<div>{count}</div>
<button onClick={increment}>+</button>
</div>
);
};
export default App;
处理异步操作
在Redux中,处理异步操作需要引入Redux Thunk或Redux Saga等中间件,理解这些库的逻辑相对复杂,心智负担极高。而在Zustand中,异步操作就像写普通的JavaScript函数一样简单,你只需要把函数标记为async异步函数即可,下面是一个例子。
store/useUserStore.js
import { create } from "zustand";
const useUserStore = create((set) => ({
userList: [],
loading: false,
error: null,
fetchUserList: async () => {
set({ loading: true, error: null });
try {
const response = await fetch("/api/getUserList");
const data = await response.json();
set({ userList: data, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
createUser: async (userData) => {
set({ loading: true, error: null });
try {
const response = await fetch("/api/createUser", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData),
});
const newUser = await response.json();
set((state) => ({
userList: [...state.userList, newUser],
loading: false,
}));
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
export default useUserStore;
App.jsx
import { useEffect } from "react";
import useUserStore from "@/store/useUserStore";
const App = () => {
const { userList, loading, error, fetchUserList } = useUserStore();
useEffect(() => {
fetchUserList();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{userList.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
export default App;
使用中间件
Zustand提供了中间件机制用于扩展Store的功能,常用的内置中间件包括devtools调试工具集成、persist状态持久化等。
devtools中间件
devtools中间件可以将Zustand的Store接入浏览器的Redux DevTools扩展,方便我们进行状态调试。
import { create } from "zustand";
import { devtools } from "zustand/middleware";
const useCounterStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}),
{ name: "CounterStore" },
),
);
export default useCounterStore;
配置devtools中间件后,打开浏览器的Redux DevTools扩展即可看到Zustand Store的状态变化记录。
persist中间件
persist中间件可以将Store的状态持久化到localStorage或sessionStorage中,实现页面刷新后状态不丢失的效果,省去了我们自己手动操作这些Storage的麻烦。
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useSettingsStore = create(
persist(
(set) => ({
theme: "light",
language: "zh-cn",
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: "app-settings",
},
),
);
export default useSettingsStore;
上面代码中,persist中间件的第2个参数是配置对象,name指定了在localStorage中存储的键名。当用户修改主题或语言设置后,刷新页面这些设置仍然会保留。除此之外,persist中间件还支持更多配置项,下面是一些例子。
persist(
(set) => ({
// ...
}),
{
name: "app-settings",
storage: createJSONStorage(() => sessionStorage), // 使用sessionStorage
partialize: (state) => ({ theme: state.theme }), // 只持久化部分状态
}
);
组合使用多个中间件
多个中间件可以嵌套组合使用。
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
const useSettingsStore = create(
devtools(
persist(
(set) => ({
theme: "light",
setTheme: (theme) => set({ theme }),
}),
{ name: "app-settings" },
),
{ name: "SettingsStore" },
),
);
export default useSettingsStore;
Store拆分
随着应用规模的增长,将所有状态放在一个Store中会导致代码臃肿、难以维护。Store维护的最佳实践是按照业务领域将状态拆分到多个独立的Store中,每个Store管理一个特定领域的状态,这也是Zustand推荐的做法。
src
|_ store
|_ useCounterStore.js
|_ useUserStore.js
|_ useSettingsStore.js
|_ ...
|_ App.jsx
|_ main.jsx
这种组织方式和Redux中将所有Slice合并到一个Store不同,Zustand中每个Store都是独立的,组件按需引入即可。这种方式的好处是各个Store之间天然解耦,代码组织清晰。
Store拆分后,一般来说是不需要互相访问的,如果有这种需求通常意味着拆分不合理。不过如果确实需要在一个Store中访问另一个Store的数据,Zustand也支持直接在Store外部调用另一个Store的getState()方法,该方法用于在React组件外部获取状态,它不会建立订阅关系,仅用于一次性读取。
import useUserStore from "./useUserStore";
const useOrderStore = create((set) => ({
orders: [],
createOrder: (orderData) => {
const currentUser = useUserStore.getState().currentUser;
// ...
},
}));