基础类型 → 引用类型 → 监听 → useShallow → 异步 → immer → persist 全流程
最近公司某些项目需要 vue 迁移到 react 技术栈,技术调研 react 的状态管理工具 zustand,相较于 redux 简便不少,具体的计较可以查看 zustand 官方文档,下面主要讲解下 zustand 的基本用法。以下例子采用的是 zustand@4.5.2 版本。
npm install zustand
前提:在 src 目录下创建 store 文件夹用于统一管理状态,store 创建 useTest.ts 文件,内容如下:
import { create } from "zustand";
interface Istore {
testName: string;
setTestName: (str: string) => void;
}
export const useTestStore = create<Istore>()((set) => ({
testName: "",
setTestName: (str: string) => set(() => ({ testName: str })),
}));
创建 test.tsx 主页面,和组件 testA、testB 组件:
import TestA from "./a";
import TestB from "./b";
const Test: React.FC = () => {
return (
<div>
<TestA></TestA>
<hr></hr>
<TestB></TestB>
</div>
);
};
export default Test;
import { useTestStore } from "@/store/useTest";
const TestA: React.FC = () => {
const { testName, setTestName } = useTestStore();
return (
<div>
<span>testName:{testName}</span>
<button onClick={() => setTestName("testA")}>更改testName</button>
</div>
);
};
export default TestA;
import { useTestStore } from "@/store/useTest";
const TestB: React.FC = () => {
const { testName, setTestName } = useTestStore();
return (
<div>
<span>testName:{testName}</span>
<button onClick={() => setTestName("testB")}>更改testName</button>
</div>
);
};
export default TestB;
此时可见两组间共享状态,一个变动另一个自动刷新。
useTest.ts:
import { create } from "zustand";
interface Iinfo {
name: string;
age: number;
}
interface Istore {
testName: string;
userInfo: Iinfo;
setTestName: (str: string) => void;
setUserInfo: (data: Iinfo) => void;
setUserInfoName: (str: string) => void;
setUserInfoAge: (str: number) => void;
}
export const useTestStore = create<Istore>()((set) => ({
testName: "默认值",
setTestName: (str: string) => set(() => ({ testName: str })),
userInfo: {
name: "张三",
age: 18,
},
setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
setUserInfoName: (name: string) =>
set((state) => ({ userInfo: { ...state.userInfo, name } })),
setUserInfoAge: (age: number) =>
set((state) => ({ userInfo: { ...state.userInfo, age } })),
}));
testA 组件:
import { useTestStore } from "@/store/useTest";
const TestA: React.FC = () => {
const {
userInfo,
setUserInfo,
setUserInfoName,
setUserInfoAge,
testName,
setTestName,
} = useTestStore();
return (
<div>
<div>testName:{testName}</div>
<div>userInfoName:{userInfo.name}</div>
<div>userInfoAge:{userInfo.age}</div>
<button onClick={() => setTestName("testA")}>更改testName</button>
<button onClick={() => setUserInfo({ name: "更改了", age: 30 })}>
更改userInfo
</button>
<button onClick={() => setUserInfoName("李四")}>更改userInfoName</button>
<button onClick={() => setUserInfoAge(90)}>更改userInfoAge</button>
</div>
);
};
export default TestA;
testB:
import { useTestStore } from "@/store/useTest";
const TestB: React.FC = () => {
const { userInfo, testName, setTestName } = useTestStore();
return (
<div>
<div>testName:{testName}</div>
<div>userInfoName:{userInfo.name}</div>
<div>userInfoAge:{userInfo.age}</div>
<button onClick={() => setTestName("testB")}>更改testName</button>
</div>
);
};
export default TestB;
此时可见 AB 组件引用类型更新。
加一个计数器,每次点击 store 加一,需要在 action 获取 state,用法没变化:
import { create } from "zustand";
interface Iinfo {
name: string;
age: number;
}
interface Istore {
count: number;
testName: string;
userInfo: Iinfo;
setTestName: (str: string) => void;
setUserInfo: (data: Iinfo) => void;
setUserInfoName: (str: string) => void;
setUserInfoAge: (str: number) => void;
setCount: () => void;
}
export const useTestStore = create<Istore>()((set) => ({
testName: "默认值",
setTestName: (str: string) => set(() => ({ testName: str })),
userInfo: {
name: "张三",
age: 18,
},
setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
setUserInfoName: (name: string) =>
set((state) => ({ userInfo: { ...state.userInfo, name } })),
setUserInfoAge: (age: number) =>
set((state) => ({ userInfo: { ...state.userInfo, age } })),
count: 0,
setCount: () => set((state) => ({ count: state.count + 1 })),
}));
A 组件变更了 count 计数器,B 组件需要监听 count 变动,回调函数,监听全部的 store 变化和特定的 count 变化。B 组件代码如下,组件卸载调用取消监听。useTestStore.subscribe 返回值为取消监听,调用即可取消监听。
import { useEffect } from "react";
import type { Istore } from "@/store/useTest";
import { useTestStore } from "@/store/useTest";
const TestB: React.FC = () => {
const { userInfo, testName, setTestName } = useTestStore();
// 在组件挂载时订阅 store 变化 只要 store 变化均可触发
useEffect(() => {
// 监听所有更改,每次更改时都会同步触发
const unsubscribeALL = useTestStore.subscribe(
(state: Istore, prevState: Istore) => {
console.log(state, prevState);
}
);
// 在组件卸载时取消订阅
return () => {
unsubscribeALL();
};
}, []);
// 在组件挂载时订阅 store 变化 仅仅监听 count 变化
useEffect(() => {
// 监听所有更改,每次更改时都会同步触发
const unsubscribeCount = useTestStore.subscribe(
(state: Istore, prevState: Istore) => {
if (state.count !== prevState.count) {
console.log(state, prevState);
}
}
);
// 在组件卸载时取消订阅
return () => {
unsubscribeCount();
};
}, []);
return (
<div>
<div>testName:{testName}</div>
<div>userInfoName:{userInfo.name}</div>
<div>userInfoAge:{userInfo.age}</div>
<button onClick={() => setTestName("testB")}>更改testName</button>
</div>
);
};
export default TestB;
在 Zustand 中,useShallow 是一个用于选择性地订阅 store 中状态变化的自定义钩子。可以选择拼装自己需要的状态,有点类似于 vue 的 computed,注意两种写法。
import { useShallow } from "zustand/react/shallow";
const TestB: React.FC = () => {
const { userInfo, testName, setTestName, count } = useTestStore();
// 自定义组装数据 1:对象写法
const { count1, userInfo1 } = useTestStore(
useShallow((state) => ({ count1: state.count, userInfo1: state.userInfo }))
);
// 自定义组装数据 2:数组写法
// const [count1, testName1] = useTestStore(
// useShallow((state) => [state.count, state.testName])
// )
return (
<div>
<div>testName:{testName}</div>
<div>count:{count}</div>
<div>userInfoName:{userInfo.name}</div>
<div>userInfoAge:{userInfo.age}</div>
<div>count1:{count1}</div>
<div>userInfo1.name:{userInfo1.name}</div>
<button onClick={() => setTestName("testB")}>更改testName</button>
</div>
);
};
export default TestB;
注意:setUserInfoNameAsync 异步设置 state。
import { create } from "zustand";
interface Iinfo {
name: string;
age: number;
}
function delayedPromise(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("异步处理名称");
}, 300);
});
}
export interface Istore {
count: number;
testName: string;
userInfo: Iinfo;
setTestName: (str: string) => void;
setUserInfo: (data: Iinfo) => void;
setUserInfoName: (str: string) => void;
setUserInfoNameAsync: () => void;
setUserInfoAge: (str: number) => void;
setCount: () => void;
}
export const useTestStore = create<Istore>()((set) => ({
testName: "默认值",
setTestName: (str: string) => set(() => ({ testName: str })),
userInfo: {
name: "张三",
age: 18,
},
setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
setUserInfoName: (name: string) =>
set((state) => ({ userInfo: { ...state.userInfo, name } })),
setUserInfoNameAsync: async () => {
const nameStr = await delayedPromise();
set((state) => ({
userInfo: { ...state.userInfo, name: nameStr },
}));
},
setUserInfoAge: (age: number) =>
set((state) => ({ userInfo: { ...state.userInfo, age } })),
count: 0,
setCount: () => set((state) => ({ count: state.count + 1 })),
}));
对用惯 vue 的来说,react 每次更改 state 需要先创建副本,再赋值 setState,操作起来麻烦,zustand 借助 immer 来简化操作。
npm i immer -S
修改 useTest.ts:
import { produce } from "immer";
import { create } from "zustand";
interface Iinfo {
name: string;
age: number;
}
interface ComplexObjectType {
nested: {
value: string;
};
array: number[];
}
function delayedPromise(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("异步处理名称");
}, 300);
});
}
export interface Istore {
count: number;
testName: string;
userInfo: Iinfo;
setTestName: (str: string) => void;
setUserInfo: (data: Iinfo) => void;
setUserInfoName: (str: string) => void;
setUserInfoNameAsync: () => void;
setUserInfoAge: (str: number) => void;
setCount: () => void;
ImmerObject: ComplexObjectType;
updateImmerObject: (updater: (draft: ComplexObjectType) => void) => void;
}
export const useTestStore = create<Istore>()((set) => ({
testName: "默认值",
setTestName: (str: string) => set(() => ({ testName: str })),
userInfo: {
name: "张三",
age: 18,
},
setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
setUserInfoName: (name: string) =>
set((state) => ({ userInfo: { ...state.userInfo, name } })),
setUserInfoNameAsync: async () => {
const nameStr = await delayedPromise();
set((state) => ({
userInfo: { ...state.userInfo, name: nameStr },
}));
},
setUserInfoAge: (age: number) =>
set((state) => ({ userInfo: { ...state.userInfo, age } })),
count: 0,
setCount: () => set((state) => ({ count: state.count + 1 })),
// immer 初始状态
ImmerObject: {
nested: {
value: "Immer",
},
array: [1, 2, 3],
},
// 使用 immer 的 produce 函数来更新复杂对象
updateImmerObject: (updater: (draft: ComplexObjectType) => void) =>
set(
produce((draft) => {
updater(draft.ImmerObject);
})
),
}));
A 组件:
import { useTestStore } from "@/store/useTest";
const TestA: React.FC = () => {
const {
userInfo,
count,
setUserInfo,
setUserInfoName,
setUserInfoAge,
testName,
setTestName,
setCount,
setUserInfoNameAsync,
ImmerObject,
updateImmerObject,
} = useTestStore();
const handleUpdate = () => {
updateImmerObject((draft) => {
draft.nested.value = "ImmerObject change";
draft.array.push(4);
});
};
return (
<div>
<div>testName:{testName}</div>
<div>userInfoName:{userInfo.name}</div>
<div>userInfoAge:{userInfo.age}</div>
<div>count:{count}</div>
<p>ImmerObject Value: {ImmerObject.nested.value}</p>
<p>ImmerObject Array: {ImmerObject.array.join(", ")}</p>
<button onClick={() => setTestName("testA")}>更改testName</button>
<button onClick={() => setUserInfo({ name: "更改了", age: 30 })}>
更改userInfo
</button>
<button onClick={() => setUserInfoName("李四")}>更改userInfoName</button>
<button onClick={() => setUserInfoAge(90)}>更改userInfoAge</button>
<button onClick={setCount}>更改count</button>
<button onClick={setUserInfoNameAsync}>setUserInfoNameAsync</button>
<button onClick={handleUpdate}>Update Immer Object</button>
</div>
);
};
export default TestA;
对于用户信息等状态,需要永久存储,否则一刷新就没了。新建一个 useUser.ts 的 store,devtools、persist 实现 local 缓存。
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
interface Iinfo {
id: string;
name: string;
token: string;
}
interface BearState {
userInfo: Iinfo;
setUserInfo: (data: Iinfo) => void;
}
export const useUserStore = create<BearState>()(
devtools(
persist(
(set) => ({
userInfo: {
id: "",
name: "",
token: "",
},
setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
}),
{
name: "userInfo",
}
)
)
);
以上就是 zustand 基础用法,入门而已。全套示例代码已在上文各节完整给出(基础类型 → 引用类型 → 计数器 → 监听 → useShallow → 异步 → immer → persist 递进组合),可直接组合成完整 store 与 TestA/TestB 页面运行。