使用 Zustand 管理你的 React 状态
前言
自从 React Hooks 出来以后, 我也全面转向了 Hooks 的怀抱, 也可能是因为大量的文章开始鼓吹 -_-!
在之前,全局状态管理并不像今天这样。我清楚地记得有一段时间,在高阶组件中的状态管理最佳选择是使用Redux, connect 加上 mapStateToProps 和 mapDispatchToProps。
到如今,Hooks 出现后一切都变了。不仅现有的解决方案变得更容易使用,而且新的解决方案也诞生了。
状态管理
目前的状态管理库大大小小都有一些差异化,也是因为这些差异化才使得我们向前跃进!
先看一下 Npm 下载 React 状态管理库的趋势:


⇪ 增长迅速~ 我很快的就去尝试使用它,在使用中也非常符合我的预期! 所以,我要向你推荐一下它!
Zustand
关于Zustand,我喜欢上它的第一点就是:小而精悍!
这是一个很小的库(v4.1.4 是 1.1KB Minified + Gzipped),它提供了一个简单的 API 来创建全局状态存储并通过selectors订阅它们。
它类似Redux, 又比Redux简易。
就像 React 本身一样, Zustand 并不固执己见。如果需要, 您可以将其与immer结合使用。
基础使用
基础使用示例
- 创建一个Store
- 获取数据
- 运行结果
/** 数据集中管理 */
import { StoreApi, UseBoundStore, create } from "zustand";
type Store = {
bears: number;
childBears: number;
count: number;
actions: Record<string, () => void>;
};
const initialState = {
count: 1,
bears: 0,
childBears: 0,
};
export const useStore = create<Store>((set) => ({
...initialState,
actions: {
incrementCount: () => set((state) => ({ count: state.count + 1 })),
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
/** 重置为初始值 */
reset: () => set(initialState),
},
}));
/** 手动创建 Selector 选择器 */
export const useBears = () => useStore((state) => state.bears);
export const useCount = () => useStore((state) => state.count);
export const useActions = () => useStore((state) => state.actions);
/** ========== 使用以下方式自动生成选择器 ========== */
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
_store: S
) => {
let store = _store as WithSelectors<typeof _store>;
store.use = {};
for (let k of Object.keys(store.getState())) {
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
}
return store;
};
export const useBearStore = createSelectors(useStore);
/** ========== 使用以上方式自动生成选择器 ========== */
export default useStore;
import React, { FunctionComponent } from "react";
import { useStore, useCount, useActions, useBears, useBearStore } from "./store";
interface BearProps {
[x: string]: any;
}
const BearChild = () => {
const bear = useStore((s) => s.bears);
// 使用手动创建的选择器 Selector
// const bear = useBears();
// 使用自动创建的选择器Selector
// const bear = useBearStore.use.bears()
const { incrementBears } = useActions();
console.log("BearChildMount");
return (
<div>
BearChild bear: {bear}
<button onClick={incrementBears}>修改Bear⬆️</button>
</div>
);
};
const BearChild2 = () => {
const childBears = useStore((s) => s.childBears);
const incrementChildBears = () =>
useStore.setState((s) => ({ childBears: s.childBears + 1 }));
console.log("BearChild2Mount");
return (
<div>
BearChild2 childBears: {childBears}
<button onClick={incrementChildBears}>修改ChildBears⬆️</button>
</div>
);
};
const Bear: FunctionComponent<BearProps> = () => {
const count = useStore((s) => s.count);
const { incrementCount } = useStore((s) => s.actions);
console.log("BearMount");
return (
<div>
<span>{count}</span>
<button onClick={incrementCount}>one up</button>
<BearChild />
<BearChild2 />
</div>
);
};
export default Bear;
-
父级组件使用时,会向下 diff 子级组件, 可以使用
memo做比较更新, 防止额外的渲染 -
子级组件使用时不会想上 diff 父级, 与 useContext 不同, 仅在使用的地方向下 Diff
-
深层嵌套对象建议使用中间件
immer, 创建下一个不可变状态树import { immer, produce } from "zustand/middleware/immer";
immerInc: () =>
set(
produce((state: State) => {
++state.deep.nested.obj.count;
})
);
为什么是 Zustand?
如果我们把状态管理当成一款产品来设计,我们不妨看看开发者在状态管理下的核心需求是什么。
我相信通过以下这一串分析,你会发现 Zustand 是真真正正满足“几乎所有”状态管理需求的工具,并且在很多细节上做到了体验优化。

状态共享
状态管理最必要的一点就是状态共享。
这也是 useContext 出来以后, 大部分文章说不需要 Redux 的根本原因。
因为 useContext 可以实现最最基础的状态共享。但这种方式(包括 Redux 在内),都需要在最外层包一个Provider。
useContext 中的值都在 Provider 的作用域下有效。
// Context 状态共享
// context.tsx
import { createContext, useContext } from 'react';
interface ContextProps {
readOnly: boolean;
disabled: boolean;
fieldKey: string;
fieldNames: Record<'title' | 'code' | 'imgUrl', string>;
}
export const Context = createContext<ContextProps>({
readOnly: false,
disabled: false,
fieldKey: 'name',
fieldNames: { title: 'productName', code: 'productCode', imgUrl: 'productImageUrl' },
});
export default useStore = () => useContext(Context);
// index.tsx
import { Context } from './context';
const App = () => {
return (
<Context.Provider value={contextValue}>
<Component />
</Context.Provider>
)
}
// Component.tsx
import useStore from './context';
const Component = () => {
const { disabled, readOnly } = useStore();
// ...
return ...
}
作为一个 Hooks 的重度拥护者,我常常会这样使用, 我觉得很方便。
但是, 我在项目中发现不少复杂 case 中使用了Provider + useContext的方式来做全局状态管理。-_-!
在复杂应用中, 性能优化、受控、action 互调、数据切片、状态调试等坑,每一项都不是好惹的主,够人喝上一壶! 最终,稍有不慎便会跌入体验差、问题多甚至死循环的深坑中。
我喜欢使用 Hooks,也因此在组内被吐槽: ‘乔克~ 最喜欢用 Hooks 了'、'乔克可是 Hooks 的重度拥护者’…
我真的是: "乔~"无奈
关于useContext的使用不在这里多讲, 我希望使用者能够正确的认识使用时机,从而避免'滥用'。
而 Zustand 做到的第一点创新就是: 默认不需要 Provider。 直接声明一个 Hooks 式的 useStore 后就可以在不同的组件中进行调用。它们的状态会直接共享,简单而美好。
// Zustand 状态共享
// store.ts
import { create } from "zustand";
export const useStore = create((set) => ({
bears: 0,
fish: 0,
eatFish: () => set((state) => ({ fish: state.fish - 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
// Control.tsx
import { useStore } from "./store";
const Control = () => {
const { fish, eatFish } = useStore();
return (
<div>
<span>{fish}</span>
<button onClick={eatFish}>吃鱼</button>
</div>
);
};
// BearCounter.tsx
import { useStore } from "./store";
const BearCounter = () => {
const bears = useStore((state) => state.bears);
return <h1>{bears} around here...</h1>;
};
由于没有 Provider 的存在,所以声明的 useStore 默认都是单实例, 如果需要多实例的话, Zustand 也提供对应的 Provider 的书写方式, 这种方式在组件库中比较常用。
此外, Zustand 的 store 状态既可以在 React 中消费,也可以在 React 外消费。
状态变更
状态管理除了状态共享外,另外第二个极其必要的能力就是状态变更。
在复杂的场景下,我们往往需要自行组织相应的状态变更方法,不然不好维护。 这也是考验一个状态管理库好不好用的一个必要指标。
Hooks 的setState 是原子级的变更状态, hold 不住复杂逻辑;
而useReducer的 Hooks 借鉴了Redux的思想, 提供了dispatch变更的方式, 但和redux的 reducer 一样, 这种方式没法处理异步, 且没法互相调用, 一旦遇上就容易捉襟见肘。
至于Redux, 哪怕是最新的 Redux-Toolkit 中优化大量Redux的模板代码, 针对同步异步方法的书写仍然让人心生畏惧。
// redux-toolkit 的用法
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { userAPI } from "./userAPI";
// 1. 创建异步函数
const fetchUserById = createAsyncThunk(
"users/fetchByIdStatus",
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId);
return response.data;
}
);
const usersSlice = createSlice({
name: "users",
initialState: { entities: [], loading: "idle" },
// 同步的 reducer 方法
reducers: {},
// 异步的 AsyncThunk 方法
extraReducers: (builder) => {
// 2. 将异步函数添加到 Slice 中
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload);
});
},
});
// 3. 调用异步方法
dispatch(fetchUserById(123));
而在 Zustand 中, 函数可以直接写, 完全不用区分同步或者异步, 一下子把区分同步异步的心智负担降到了0。
// zustand store 写法
// store.ts
import create from "zustand";
const initialState = {
// ...
};
export const useStore = create((set, get) => ({
...initialState,
createNewDesignSystem: async () => {
const { params, toggleLoading } = get();
toggleLoading();
const res = await dispatch("/hitu/remote/create-new-ds", params);
toggleLoading();
if (!res) return;
set({ created: true, designId: res.id });
},
toggleLoading: () => {
set({ loading: !get().loading });
},
}));
// CreateForm.tsx
import { useStore } from "./store";
const CreateForm: FC = () => {
const { createNewDesignSystem } = useStore();
// ...
};
另一个让人非常舒心的点在于, Zustand会默认将所有的函数保持同一引用。
所以用Zustand写的方法, 默认都不会造成额外的重复渲染。
如果你习惯使用useReducer的模式, 它可以直接集成, 而且直接在官网提供了示例。
这样就意味着你可以极低成本地完成迁移。
状态派生
状态派生是一个不被那么多人提起, 但是在实际场景中被大量使用的东西。
状态派生可以很简单, 也可以非常复杂。简单的例子, 比如基于一个name字段, 拼接出对应的值。
如果不考虑优化, 其实都可以写一个中间的函数作为派生方法, 但作为状态管理的一环, 我们必须要考虑相应的优化。
在 Hooks 场景下, 状态派生的方法可以使用 useMemo, 例如:
const App = () => {
const [name, setName] = useState("");
const tell = useMemo(() => `我的名字是: ${name || ""}`, [name]);
// ...
};
而Zustand用了类似 Redux Selector 的方法, 实现相应的状态派生, 这个方式使得 useStore 的用法变得极其灵活和使用。
而这种 Selector 的方式使得 Zustand 下细颗粒度的性能优化变为可能, 且优化成本极低。
// zustand 的 selector 用法
// 写法1
const App = () => {
const tell = useStore((s) => `我的名字是: ${s.name || ""}`);
// ...
};
// 写法2 将 selector 单独抽为函数
export const useTellSelector = () =>
useStore((s) => `我的名字是: ${s.name || ""}`);
const App = () => {
const tell = useTellSelector();
// ...
};
独立管理派生状态
由于写法 2可以将Selector抽为独立函数, 那么我们就可以将其拆分到独立文件来管理派生状态。

性能优化
讲完派生状态知道了
Zustand的Selector能力后, 就可以讲讲Zustand的性能优化了。
在裸 Hooks 的状态管理下, 要做性能优化得专门起一个专项来分析与实施。
但基于Zustand的useStore和Selector用法, 我们可以实现低成本、渐进式的性能优化。
仅导出自定义的 Hooks
这是首要技巧, 对于任何 React 开发都满足。
只要利用Zustand的Selector, 就可以很简单的做到性能优化。
export const useFish = () => useStore((state) => state.fish);
export const useEatFish = () => useStore((state) => state.eatFish);
// 💡 仅订阅指定状态变化
const Control = () => {
const fish = useFish();
const eatFish = useEatFish();
return (
<div>
<span>{fish}</span>
<button onClick={eatFish}>吃鱼</button>
</div>
);
};
此外,它还避免了意外地订阅整个状态:
// ❌ 这将订阅整个状态
const Control = () => {
const { fish, eatFish } = useStore();
return (
<div>
<span>{fish}</span>
<button onClick={eatFish}>吃鱼</button>
</div>
);
};
虽然结果可能是相同的, 上面的代码将订阅你的整个状态, 这意味着你的组件将在某个状态更新后全部重新渲染, 即使 fish 没有改变。
虽然选择器在Zustand中是可选的, 但我认为你应该始终使用它们。即使我们有一个只有一个状态值的存储, 我也会编写一个自定义钩子, 只是为了将来能够添加更多状态。
多使用原子选择器
Zustand通过比较选择器的结果和之前渲染的结果来决定何时通知组件更新。默认情况下, 它通过严格的相等检查来实现。
实际上, 这意味着选择器必须返回稳定的结果。如果返回一个新的Array或Object, 它将始终被视为更改, 即使内容相同:
// 🚨 选择器在每次调用时都会返回一个新对象
const { bears, fish } = useStore((state) => ({
bears: state.bears,
fish: state.fish,
}));
// 😮 所以这两者是等价的
const { bears, fish } = useStore();
如果要从选择器返回 Object 或 Array, 可以将比较函数调整为使用浅比较:
import shallow from "zustand/shallow";
// ⬇️ much better, because optimized
const { bears, fish } = useStore(
(state) => ({ bears: state.bears, fish: state.fish }),
shallow
);
然后我更倾向于导出两个独立的Selectors
export const useBears = () => useStore((state) => state.bears);
export const useFish = () => useStore((state) => state.fish);
假如组件需要多个值, 引入多个 Hooks 即可。
Actions 和 State 分离
Actions 是不变的函数, 它更新 State 中的值, 所以它们不是真正的"状态"。
将 Actions 作为一个额外的对象在我们的存储中分离, 将允许我们将它们作为一个单个 Hook 暴露出来, 以便在我们的任何组件中使用, 而不会对性能产生任何影响:
// store.tsx
export const useStore = create((set) => ({
bears: 0,
fish: 0,
actions: {
eatFish: () => set((state) => ({ fish: state.fish - 1 })),
removeAllBears: () => set({ bears: 0 }),
},
}));
/** 手动创建 Selector 选择器 */
export const useBears = () => useStore((state) => state.bears);
export const useFish = () => useStore((state) => state.fish);
export const useActions = () => useStore((state) => state.actions);
现在可以只使用一个 Hooks 导出所有的 actions
const App = () => {
const { eatFish } = useActions();
return (
<div>
<button onClick={eatFish}>吃鱼</button>
</div>
);
};
这看起来和上面提到的原子选择器相反, 但是事实并非如此, 由于 actions不会改变, actions可以看出是一个原子块。
Model Actions as Events, not Setters
这是一个通用的提示, 无论你是否使用useReducer、Redux或Zustand。
这个技巧直接来自Redux 风格指南。
它将帮助你将业务逻辑保存在存储中, 而不是保留在组件中。
上面的例子已经使用了这种模式——逻辑(例如: “吃鱼”)存在于状态中。组件只是调用操作, 而存储决定如何处理它。
保持状态小型化
与Redux不同的是, Zustand鼓励你拥有多个小型状态。每个状态可以对单个状态负责。如果你需要组合它们, 你可以使用自定义钩子:
const currentUser = useCredentialsStore((state) => state.currentUser);
const user = useUsersStore((state) => state.users[currentUser]);
结合其他库
老实说, 我并不经常需要组合多个Zustand状态, 因为应用程序中的大多数状态要么是服务器状态, 要么是 URL 状态。
与将两个状态组合起来相比, 我更可能将Zustand状态与useQuery或useParams结合起来。
同样的原则也适用于: 如果你需要将其他 Hook 与Zustand状态结合起来, 自定义 Hooks 也许是你最好的选择
例如: 我喜欢使用 useQuery 管理服务器状态
const useFilterStore = create((set) => ({
applied: [],
actions: {
addFilter: (filter) =>
set((state) => ({ applied: [...state.applied, filter] })),
},
}));
export const useAppliedFilters = () => useFilterStore((state) => state.applied);
export const useFiltersActions = () => useFilterStore((state) => state.actions);
// 🚀 将zustand存储与useQuery结合起来
export const useFilteredTodos = () => {
const filters = useAppliedFilters();
return useQuery({
queryKey: ["todos", filters],
queryFn: () => getTodos(filters),
});
};
在这里, 应用的筛选器驱动查询, 因为筛选器是查询键的一部分。每次调用addFilter(可以在 UI 中的任何地方执行)时, 都会自动触发一个新查询, 这个查询也可以在 UI 中的任何地方使用。我觉得这事一个非常简洁明了的说明, 也不神奇。
数据分形与状态组合
如果子组件能够以同样的结构, 作为一个应用使用, 这样的结构就是分形架构。
数据分形在状态管理里我觉得是个比较高级的概念。但从应用上来说很简单, 就是更容易拆分并组织代码, 而且具有更加灵活的使用方式,
常用例如将多个小型 Store 进行组合。
// 第一个 Store:
export const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});
//另一个 Store:
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
});
现在可以将这两个存储合并到一个 Store 中:
import { create } from "zustand";
import { createBearSlice } from "./bearSlice";
import { createFishSlice } from "./fishSlice";
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}));
在 React 组件中的用法
import { useBoundStore } from "./stores/useBoundStore";
const App = () => {
const bears = useBoundStore((state) => state.bears);
const fishes = useBoundStore((state) => state.fishes);
const addBear = useBoundStore((state) => state.addBear);
return (
<div>
<h2>Number of bears: {bears}</h2>
<h2>Number of fishes: {fishes}</h2>
<button onClick={() => addBear()}>Add a bear</button>
</div>
);
};
分形架构下的各种中间件
基于这种分形架构, 状态就具有了很灵活的组合性, 例如将当前状态直接缓存到 localStorage。
在Zustand的架构下, 不用额外改造, 直接加个persist中间件就好。
// 使用自带的 Persist Middleware
import { create } from "zustand";
import { createBearSlice } from "./bearSlice";
import { createFishSlice } from "./fishSlice";
import { persist } from "zustand/middleware";
export const useBoundStore = create(
persist(
(...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}),
{ name: "bound-store" }
)
);
多环境集成(React 内外环境联动)
实际的复杂应用中, 一定会存在某些不在 React 环境内的状态数据,以图表、画布、3D 场景最多。
一旦涉及到多环境下的状态管理, 可以让人掉无数头发。
而Zustand说了, 不慌, 我已经考虑到了, useStore上直接可以拿值, 是不是很贴心~
// 官方示例
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
// 1. 创建Store
const useStore = create(
subscribeWithSelector(() => ({ paw: true, snout: true, fur: true }))
)
// 2. react 环境外直接拿值
const paw = useStore.getState().paw
// 3. 提供外部事件订阅
const unsub1 = useStore.subscribe(console.log)
const unsub2 = useStore.subscribe((state) => state.paw, console.log)
// 4. react 世界外更新值
useStore.setState({ paw: false })
unsub1()
unsub2()
// 销毁商店(移除所有监听器)
useStore.destroy();
// 5. 在 react 环境内使用
const Component = () => {
const paw = useStore((state) => state.paw)
...
}
总结: Zustand 是当下复杂状态管理的最佳选择
我一直在寻求最佳的状态管理库, 直到看到Zustand让我眼前一亮。
我很笃定地认为, Zustand就是我当下状态管理的最佳选择, 甚至是大部分复杂应用的状态管理的最佳选择。