危险

为之则易,不为则难

0%

Zustand

🐻 A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn’t boilerplatey or opinionated, but has enough convention to be explicit and flux-like.

环境

1
2
npm create vite@latest
npm i @types/node -D

vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { join } from "node:path";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": join(__dirname, "src"),
},
},
});

tsconfig.app.json

1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
// ...
}
// ...
}

获取数据

1
npm i zustand

src/store/index.ts

1
2
3
4
5
6
7
8
9
import { create } from "zustand";
// 通过 create 生成创建 store 的 hook
const useStore = create(() => {
return {
bears: 0,
};
});

export default useStore;

src/components/setup.tsx

1
2
3
4
5
6
7
8
9
10
11
12
import { FC } from "react";
import useStore from "@/store";

export const Father: FC = () => {
const brears = useStore((state) => state.bears);
return (
<>
<h3>Father 组件</h3>
<p>Bears: {brears}</p>
</>
);
};

src/App.tsx

1
2
3
4
5
6
import { Father } from "@/components/setup";
const App = () => {
return <Father />;
};

export default App;

添加类型提示

1. 在 vite-env.d.ts 中添加 store 的类型声明;

1
2
3
4
/// <reference types="vite/client" />
type BearType = {
bears: number;
};

2. 在 src/store/index.ts 中调用 create 函数时,使用类型声明;

1
2
3
4
5
6
7
8
9
10
import { create } from "zustand";

// 可以直接使用 BearType,无需导入
const useStore = create<BearType>()(() => {
return {
bears: 0,
};
});

export default useStore;

3. 改造完成后,在组件中使用 selector 选择器获取数据时,就有了 TS 的类型提示。

修改 bears 的数量

1. 在 vite-env.d.ts 中为 BearType 添加名为 incrementBears 的属性,它是一个函数;

1
2
3
4
5
/// <reference types="vite/client" />
type BearType = {
bears: number;
incrementBears: () => void;
};

2. 在 src/store/index.ts 中新增 incrementBears 函数如下;

1
2
3
4
5
6
7
8
9
10
import { create } from "zustand";

const useStore = create<BearType>()((set) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
};
});

export default useStore;

3. 在 Son 组件中,调用 useStore 并配合 selector 获取到需要的函数,并绑定为按钮的点击事件处理函数。

src/components/setup.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { FC } from "react";
import useStore from "@/store";

const Son: FC = () => {
const incrementBears = useStore((state) => state.incrementBears);
return (
<>
<h5>Son 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
</>
);
};

export const Father: FC = () => {
const brears = useStore((state) => state.bears);
return (
<>
<h3>Father 组件</h3>
<p>Bears: {brears}</p>
<hr />
<Son />
</>
);
};

重置 bears 的数量

1. 在 vite-env.d.ts 中为 BearType 添加名为 resetBears 的属性;

1
2
3
4
5
6
/// <reference types="vite/client" />
type BearType = {
bears: number;
incrementBears: () => void;
resetBears: () => void;
};

2. 在 src/store/index.ts 中新增 reset 函数;

1
2
3
4
5
6
7
8
9
10
11
import { create } from "zustand";

const useStore = create<BearType>()((set) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
resetBears: () => set({ bears: 0 }),
};
});

export default useStore;

3. 在 Son 组件中,调用 useStore 并配合 selector 获取到需要的函数,如下:

src/components/setup.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { FC } from "react";
import useStore from "@/store";

// ...

const Son: FC = () => {
const incrementBears = useStore((state) => state.incrementBears);
const resetBears = useStore((state) => state.resetBears);
return (
<>
<h5>Son1 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
</>
);
};

根据 step 使 bears 数量自减

1. 在 vite-env.d.ts 中为 BearType 添加名为 decrementBearsByStep 的属性;

1
2
3
4
5
6
7
/// <reference types="vite/client" />
type BearType = {
bears: number;
incrementBears: () => void;
resetBears: () => void;
decrementBearsByStep: (step?: number) => void;
};

2. 在 src/store/index.ts 中新增 decrementBearsByStep 函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
import { create } from "zustand";

const useStore = create<BearType>()((set) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
resetBears: () => set({ bears: 0 }),
decrementBearsByStep: (step = 1) =>
set((state) => ({ bears: state.bears - step })),
};
});

export default useStore;

3. 使用 decrementBearsByStep 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { FC } from "react";
import useStore from "@/store";

// ...

const Son: FC = () => {
const incrementBears = useStore((state) => state.incrementBears);
const resetBears = useStore((state) => state.resetBears);
const decrementBearsByStep = useStore((state) => state.decrementBearsByStep);
return (
<>
<h5>Son1 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
<button onClick={() => decrementBearsByStep(3)}>decrement bears</button>
</>
);
};

异步修改 bears 的数量

1. 定义类型;

1
2
3
4
5
6
7
8
/// <reference types="vite/client" />
type BearType = {
bears: number;
incrementBears: () => void;
resetBears: () => void;
decrementBearsByStep: (step?: number) => void;
asyncIncrementBears: () => void;
};

2. 在 src/store/index.ts 中新增 asyncIncrementBears 函数如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { create } from "zustand";

const useStore = create<BearType>()((set, get) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
resetBears: () => set({ bears: 0 }),
decrementBearsByStep: (step = 1) =>
set((state) => ({ bears: (state.bears -= step) })),
asyncIncrementBears: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
// set((state) => ({ bears: state.bears + 1 }));
get().incrementBears();
},
};
});

export default useStore;

3. 使用 asyncIncrementBears。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { FC } from "react";
import useStore from "@/store";

// ...

const Son: FC = () => {
const incrementBears = useStore((state) => state.incrementBears);
const resetBears = useStore((state) => state.resetBears);
const decrementBearsByStep = useStore((state) => state.decrementBearsByStep);
const asyncIncrementBears = useStore((state) => state.asyncIncrementBears);
return (
<>
<h5>Son1 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
<button onClick={() => decrementBearsByStep(3)}>decrement bears</button>
<button onClick={asyncIncrementBears}>async add 1</button>
</>
);
};

添加 fishes 相关的共享数据

1. 修改 vite-env.d.ts 中的 BearType 类型,添加 fishes 相关的数据和方法;

1
2
3
4
5
6
7
8
9
10
11
/// <reference types="vite/client" />
type BearType = {
bears: number;
incrementBears: () => void;
resetBears: () => void;
decrementBearsByStep: (step?: number) => void;
asyncIncrementBears: () => void;
fishes: number;
incrementFishes: () => void;
resetFishes: () => void;
};

2. 修改 @/store/index.ts 模块,添加 fishes 相关的数据和方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { create } from "zustand";

const useStore = create<BearType>()((set, get) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
resetBears: () => set({ bears: 0 }),
decrementBearsByStep: (step = 1) =>
set((state) => ({ bears: (state.bears -= step) })),
asyncIncrementBears: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
// set((state) => ({ bears: state.bears + 1 }));
get().incrementBears();
},
fishes: 0,
incrementFishes: () => set((state) => ({ fishes: state.fishes + 1 })),
resetFishes: () => set({ fishes: 0 }),
};
});

export default useStore;

3. 在 @/components/ 目录下新建 fishes.tsx 模块,在模块中创建名为 Fishes 的组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import useStore from "@/store";
import { FC } from "react";

export const Fishes: FC = () => {
const fishes = useStore((state) => state.fishes);
const incrementFishes = useStore((state) => state.incrementFishes);
const resetFishes = useStore((state) => state.resetFishes);
return (
<>
<h5>小鱼干数量:{fishes}</h5>
<button onClick={incrementFishes}>fishes+1</button>
<button onClick={resetFishes}>reset fishes</button>
</>
);
};

4. 修改 @/App.tsx 组件,导入并使用 Fishes 组件。

1
2
3
4
5
6
7
8
9
10
11
12
import { Father } from "@/components/setup";
import { Fishes } from "@/components/fishes";
const App = () => {
return (
<>
<Father />
<Fishes />
</>
);
};

export default App;

拆分 store - Multi-Store

🤠 Multi-Store:把不同的数据和方法拆分为多个彼此独立的 store,像 Pinia。

🤡 Single-Store:把不同的数据和方法拆分为多个 slice 切片,最后合并成全局唯一的 Store,像 Redux。

拆分步骤

1. 修改 vite-env.d.ts 文件,把 BearType 和 FishType 拆分为两个独立的类型声明;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <reference types="vite/client" />
type BearStoreType = {
bears: number;
incrementBears: () => void;
resetBears: () => void;
decrementBearsByStep: (step?: number) => void;
asyncIncrementBears: () => void;
};

type FishStoreType = {
fishes: number;
incrementFishes: () => void;
resetFishes: () => void;
};

2. 在 @/store/ 目录下新建 bearStore.ts 模块,用来声明 bears 相关的 Store 数据,然后把 @/store/index.ts 中的代码粘贴到 bearStore.ts 中进行改造(特别注意:要把 useStore 更名为 useBearStore):

src/store/bearStore.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { create } from "zustand";

const useBearStore = create<BearStoreType>()((set, get) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
resetBears: () => set({ bears: 0 }),
decrementBearsByStep: (step = 1) =>
set((state) => ({ bears: (state.bears -= step) })),
asyncIncrementBears: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
// set((state) => ({ bears: state.bears + 1 }));
get().incrementBears();
},
};
});

export default useBearStore;

3. 在 @/store/ 目录下新建 fishStore.ts 模块,用来声明 fishes 相关的 Store 数据。然后把 @/store/index.ts 中的代码粘贴到 fishStore.ts 中进行改造(特别注意:要把 useStore 更名为 useFishStore):

1
2
3
4
5
6
7
8
9
10
11
import { create } from "zustand";

const useFishStore = create<FishStoreType>()((set) => {
return {
fishes: 0,
incrementFishes: () => set((state) => ({ fishes: state.fishes + 1 })),
resetFishes: () => set({ fishes: 0 }),
};
});

export default useFishStore;

4. 删除 @/store/index.ts 模块;

5. 改造 @/components/setup.tsx 模块,把 import useStore from ‘@/store’ 的模块导入改为 import useBearStore from ‘@/store/bearStore’,并将所有用到 useStore 的地方更名为 useBearStore;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { FC } from "react";
import useBearStore from "@/store/bearStore";

export const Father: FC = () => {
const brears = useBearStore((state) => state.bears);
return (
<>
<h3>Father 组件</h3>
<p>Bears: {brears}</p>
<hr />
<Son />
</>
);
};

const Son: FC = () => {
const incrementBears = useBearStore((state) => state.incrementBears);
const resetBears = useBearStore((state) => state.resetBears);
const decrementBearsByStep = useBearStore(
(state) => state.decrementBearsByStep
);
const asyncIncrementBears = useBearStore(
(state) => state.asyncIncrementBears
);
return (
<>
<h5>Son1 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
<button onClick={() => decrementBearsByStep(3)}>decrement bears</button>
<button onClick={asyncIncrementBears}>async add 1</button>
</>
);
};

6. 改造 @/components/fishes.tsx 模块,把 import useStore from ‘@/store’ 的模块导入改为 import useFishStore from ‘@/store/fishStore’,并将所有用到 useStore 的地方更名为 useFishStore。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import useFishStore from "@/store/fishStore";
import { FC } from "react";

export const Fishes: FC = () => {
const fishes = useFishStore((state) => state.fishes);
const incrementFishes = useFishStore((state) => state.incrementFishes);
const resetFishes = useFishStore((state) => state.resetFishes);
return (
<>
<h5>小鱼干数量:{fishes}</h5>
<button onClick={incrementFishes}>fishes+1</button>
<button onClick={resetFishes}>重置小鱼干的数量</button>
</>
);
};

持久化

zustand 内置了数据持久化的 persist 中饲件,对于 Muti-Store 中的每个 Store,我们可以自行决定是否对其进行持久化存储。例如,下面的代码演示了如何对 bearStore 进行持久化:

src/store/bearStore.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { create } from "zustand";
// #1
import { persist } from "zustand/middleware";

const useBearStore = create<BearStoreType>()(
// #2
persist(
(set, get) => {
return {
bears: 0,
incrementBears: () => set((state) => ({ bears: state.bears + 1 })),
resetBears: () => set({ bears: 0 }),
decrementBearsByStep: (step = 1) =>
set((state) => ({ bears: (state.bears -= step) })),
asyncIncrementBears: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
// set((state) => ({ bears: state.bears + 1 }));
get().incrementBears();
},
};
},
{ name: "bear-store" }
)
);

export default useBearStore;

src/store/fishStore.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { create } from "zustand";
import { persist } from "zustand/middleware";

const useFishStore = create<FishStoreType>()(
persist(
(set) => {
return {
fishes: 0,
incrementFishes: () => set((state) => ({ fishes: state.fishes + 1 })),
resetFishes: () => set({ fishes: 0 }),
};
},
{
name: "fish-store",
storage: createJSONStorage(() => sessionStorage),
}
)
);

export default useFishStore;

在 Redux DevTools 中调试 Store 中数据

src/store/fishStore.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { create } from "zustand";
// #1
import { createJSONStorage, persist, devtools } from "zustand/middleware";

const useFishStore = create<FishStoreType>()(
// #2
devtools(
persist(
(set) => {
return {
fishes: 0,
incrementFishes: () => set((state) => ({ fishes: state.fishes + 1 })),
resetFishes: () => set({ fishes: 0 }),
};
},
{
name: "fish-store",
storage: createJSONStorage(() => sessionStorage),
}
),
{
// #3
name: "fishStore",
}
)
);

export default useFishStore;

使用 immer 简化数据操作

1. 安装 immer;

1
npm i immer

2. 按需导入 immer 中间件,src/store/bearStore.ts;

1
import { immer } from 'zustand/middleware/immer'

3. 在 create 中调用 immer 中间件,基于 immer 的语法,简化数据的变更操作。在 set(fn) 的 fn 回调函数中,可以直接修改原数据对象,下面的代码是基于 immer 语法修改后的 actions 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
// #1
import { immer } from "zustand/middleware/immer";

// 通过 create 生成创建 store 的 hook
const useBearStore = create<BearStoreType>()(
devtools(
// #2
immer(
persist(
(set, get) => {
return {
bears: 0,
incrementBears: () =>
set((state) => {
// #3
state.bears += 1;
}),
resetBears: () => set({ bears: 0 }),
decrementBearsByStep: (step = 1) =>
set((state) => {
// #4
state.bears -= step;
}),
asyncIncrementBears: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
get().incrementBears();
},
};
},
{
name: "bear-store",
}
)
),
{
name: "bearStore",
}
)
);

export default useBearStore;

官方对于中间件调用的建议

https://docs.pmnd.rs/zustand/guides/typescript#using-middlewares

Also, we recommend using devtools middleware as last as possible. For example, when you use it with immer as a middleware, it should be devtools(immer(…)) and not immer(devtools(…)). This is becausedevtools mutates the setState and adds a type parameter on it, which could get lost if other middlewares (like immer) also mutate setState before devtools. Hence using devtools at the end makes sure that no middlewares mutate setState before it.

从 fishStore 中抽离 Action 函数

目前在 bearStore 和 fishStore 中,数据和函数定义在一起,随着项目规模的扩大,每个 Store 的结构会显得比较混乱。我们可以把 Action 从 Store 的 create 函数中抽离出来,使代码结构更加清晰。

1. 修改 vite-env.d.ts 文件中的 FishType 类型,把它下面所有的 Action 函数的类型注释或删除掉;

1
2
3
4
5
6
type FishStoreType = {
fishes: number;
// #1
// incrementFishes: () => void
// resetFishes: () => void
};

2. 修改 @/store/fishStore.ts 模块,把 create() 中的 Action 函数单独抽离出来;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { create } from "zustand";
import { createJSONStorage, persist, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

// 通过 create 生成创建 store 的 hook
const useFishStore = create<FishStoreType>()(
devtools(
immer(
persist(
() => {
return {
fishes: 0,
// #1 剪切
};
},
{
name: "fish-store",
storage: createJSONStorage(() => sessionStorage),
}
)
),
{
name: "fishStore",
}
)
);

export default useFishStore;

// #2
export const incrementFishes = () => {
useFishStore.setState((state) => {
state.fishes += 1;
});
};

// #3
export const resetFishes = () => {
useFishStore.setState((state) => {
state.fishes = 0;
});
};

3. 改造 @components/fishes.tsx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// #4
import useFishStore, { incrementFishes, resetFishes } from "@/store/fishStore";
import { FC } from "react";

export const Fishes: FC = () => {
const fishes = useFishStore((state) => state.fishes);
// #5 删除
// ...
return (
<>
<h5>小鱼干数量:{fishes}</h5>
<button onClick={incrementFishes}>fishes+1</button>
<button onClick={resetFishes}>重置小鱼干的数量</button>
</>
);
};

从 bearStore 中抽离 Action 函数

1. 修改 vite-env.d.ts 文件中的 BearStoreType 类型,把它下面所有的 Action 函数的美型注释或鼎除掉;

1
2
3
4
5
6
7
8
9
/// <reference types="vite/client" />
type BearStoreType = {
bears: number;
// #1
// incrementBears: () => void;
// resetBears: () => void;
// decrementBearsByStep: (step?: number) => void;
// asyncIncrementBears: () => void;
};

2. 修改 @/store/bearStore.ts 模块,把 create() 中的 Action 函数单独抽离出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

const useBearStore = create<BearStoreType>()(
devtools(
// #2
immer(
persist(
() => {
return {
bears: 0,
// #3 剪切
};
},
{
name: "bear-store",
}
)
),
{
name: "bearStore",
}
)
);

export default useBearStore;

export const incrementBears = () => {
useBearStore.setState((state) => {
// 注意 immer 的写法,外层的 {} 是必须的,否则会报错
state.bears += 1;
});
};
export const resetBears = () => useBearStore.setState({ bears: 0 });
export const decrementBearsByStep = (step = 1) => {
useBearStore.setState((state) => {
state.bears -= step;
});
};
export const asyncIncrementBears = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
incrementBears();
};

3. 改造 src/components/setup.tsx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { FC } from "react";
import useBearStore, {
incrementBears,
resetBears,
decrementBearsByStep,
asyncIncrementBears,
} from "@/store/bearStore";

const Son: FC = () => {
return (
<>
<h5>Son1 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
<button onClick={() => decrementBearsByStep(3)}>decrement bears</button>
<button onClick={asyncIncrementBears}>async add 1</button>
</>
);
};

// ...

重置所有数据

方法 1

src/components/setup.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { FC } from "react";
import useBearStore, {
incrementBears,
resetBears,
decrementBearsByStep,
asyncIncrementBears,
} from "@/store/bearStore";

// #1
import { resetFishes } from "@/store/fishStore";

const Son: FC = () => {
// #2
const resetAll = () => {
resetBears();
resetFishes();
};
return (
<>
<h5>Son 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
<button onClick={() => decrementBearsByStep(3)}>decrement bears</button>
<button onClick={asyncIncrementBears}>async add 1</button>
<button onClick={resetAll}>重置所有数据</button>
</>
);
};

// ...

方法 2

1. 在 @/store 目录下新建 tools/ 文件夹,并在 @/store/tools/ 目录下新建 resetters.ts 模块,代码如下:

1
2
3
const resetters: (() => void)[] = [];

export default resetters;

2. 修改 @/store/bearStore.ts 模块,提供初始数据和添加 resetter 函数,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
// #1
import resetters from "@/store/tools/resetters";

// #2
const initBearState = {
bears: 0,
};

const useBearStore = create<BearStoreType>()(
devtools(
immer(
persist(
(set) => {
// #4
resetters.push(() => set(initBearState));
return {
// #3
...initBearState,
};
},
{
name: "bear-store",
}
)
),
{
name: "bearStore",
}
)
);

export default useBearStore;

// ...

3. 修改 store/fishStore.ts 代码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { create } from "zustand";
import { createJSONStorage, persist, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
// #5
import resetters from "./tools/resetters";

// #6
const initFishState = {
fishes: 0,
};

const useFishStore = create<FishStoreType>()(
devtools(
immer(
persist(
(set) => {
// #8
resetters.push(() => set(initFishState));
return {
// #7
...initFishState,
};
},
{
name: "fish-store",
storage: createJSONStorage(() => sessionStorage),
}
)
),
{
name: "fishStore",
}
)
);

export default useFishStore;

// ...

4. 修改 /store/tools/resetters.ts 中的代码,向外按需导出一个名为 resetAllStore 的函数;

1
2
3
4
5
const resetters: (() => void)[] = [];
export default resetters;

// #9 重置所有 store 的函数
export const resetAllStores = () => resetters.forEach((reset) => reset());

5. 测试重置所有 Store 的功能:修改 @/components/setup.tsx 模块,先按需导入 resetAllStore 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { FC } from "react";
import useBearStore, {
incrementBears,
resetBears,
decrementBearsByStep,
asyncIncrementBears,
} from "@/store/bearStore";

// #1
import { resetAllStores } from "@/store/tools/resetters";
const Son: FC = () => {
return (
<>
<h5>Son 组件</h5>
<button onClick={incrementBears}>bears + 1</button>
<button onClick={resetBears}>reset bears</button>
<button onClick={() => decrementBearsByStep(3)}>decrement bears</button>
<button onClick={asyncIncrementBears}>async add 1</button>
{/* #2 */}
<button onClick={resetAllStores}>重置所有数据</button>
</>
);
};

// ...

在组件之外访问 Store 中的数据

在 React 组件中,可以基于 store hook 的 selector 选择器,轻松获取并使用 store 中的数据,例如:

1
2
3
4
5
6
7
8
9
10
11
12
import useBearStore from "@/store/bearStore";
export const Father: FC = () => {
const brears = useBearStore((state) => state.bears);
return (
<>
<h3>Father 组件</h3>
<p>Bears: {brears}</p>
<hr />
<Son />
</>
);
};

实际开发中,我们常需要在组件之外访问 store 中的数据,例如:在 axios 的拦截器中访问 store 中的 token 等其它数据。此时,可以使用 store hook 的 getState 方法拿到 store 的数据对象,并访问具体的数据,语法格式如下:

1
2
3
4
5
import useBearStore from "@/store/bearStore";
import useFishStore from "@/store/fishStore";

useBearStore.getState().bears;
useFishStore.getState().fishes;

例如,在 axios 的请求拦战器中,为请求头挂载 beas 的数量:

1
2
3
4
5
6
7
8
9
10
11
import useBearStore from "@/store/bearStore";

axios.interceptors.request.use(
(config) => {
config.headers.bears = useBearStore.getState().bears;
return config;
},
(error) => {
return Promise.reject(error);
}
);

🎉 在组件之外访问 store 的 Actions,只需按需导入对应的 Action 函数即可使用。

添加 familyStore 相关的功能

创建 familyStore 模块

1. 修改 @/vite-env.d.ts 模块,新增 FamilyType 类型如下:

1
2
3
4
5
6
7
type FamilyStoreType = {
family: {
father: string;
mother: string;
son: string;
};
};

2. 在 store 目录下新建 familyStore.ts 模块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { create } from "zustand";

const initFamilyState: FamilyStoreType = {
family: {
father: "Ifer",
mother: "Xxx",
son: "Yyy",
},
};

// 创建 Store 的 Hook
const useFamilyStore = create<FamilyStoreType>()(() => {
return {
...initFamilyState,
};
});

export default useFamilyStore;

为 familyStore 配置中间件

1. 配置 persist 中间件,src/store/familyStore.ts;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { persist } from "zustand/middleware";

// ...

const useFamilyStore = create<FamilyStoreType>()(
persist(
() => {
return {
...initFamilyState,
};
},
{
name: "family-store",
}
)
);

// ...

2. 配置 immer 中间件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
import { immer } from "zustand/middleware/immer";

const useFamilyStore = create<FamilyStoreType>()(
immer(
persist(
() => {
return {
...initFamilyState,
};
},
{
name: "family-store",
}
)
)
);

3. 配置 devtools 中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

const initFamilyState: FamilyStoreType = {
family: {
father: "Ifer",
mother: "Xxx",
son: "Yyy",
},
};

// 创建 Store 的 Hook
const useFamilyStore = create<FamilyStoreType>()(
devtools(
immer(
persist(
() => {
return {
...initFamilyState,
};
},
{
name: "family-store",
}
)
),
{
name: "familyStore",
}
)
);

export default useFamilyStore;

实现 family 组件的相关功能

1. 在 @/components 目录下新建 family.tsx 模块,并在内部创建 FamilyWrapper、FamilyMembers 和 FamilyNames 的 3 个组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { FC } from "react";

export const FamilyWrapper: FC = () => {
return (
<>
<FamilyMembers />
<hr />
<FamilyNames />
</>
);
};

const FamilyMembers: FC = () => {
return (
<>
<h5>小熊一家的成员:</h5>
</>
);
};

const FamilyNames: FC = () => {
return (
<>
<h5>熊熊的名字:</h5>
</>
);
};

2. 在 @/App.tsx 根组件中,按需导入并使用 FamilyWrapper 组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Father } from "@/components/setup";
import { Fishes } from "@/components/fishes";
import { FamilyWrapper } from "@/components/family";
const App = () => {
return (
<>
<Father />
<Fishes />
<hr />
<FamilyWrapper />
</>
);
};

export default App;

3. 在 @/components/family.tsx 中按需导入 useFamilyStore hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { FC } from "react";
// #1
import useFamilyStore from "@/store/familyStore";
export const FamilyWrapper: FC = () => {
return (
<>
<FamilyMembers />
<hr />
<FamilyNames />
</>
);
};

const FamilyMembers: FC = () => {
// #2
const members = useFamilyStore((state) => Object.keys(state.family));
return (
<>
{/* #3 */}
<h5>小熊一家的成员:{members.join(", ")}</h5>
</>
);
};

const FamilyNames: FC = () => {
// #4
const names = useFamilyStore((state) => Object.values(state.family));
return (
<>
{/* #5 */}
<h5>熊熊的名字:{names.join(", ")}</h5>
</>
);
};

修改 family 中 son 的名字

1. 修改 @/store/familyStore.ts 模块,新增名为 updateSonName 的 Action 函数;

1
2
3
4
5
6
// ...
export const updateSonName = (sonName: string) => {
useFamilyStore.setState((state) => {
state.family.son = sonName;
});
};

2. 修改 @/components/family.tsx 模块下的 FamilyNames 组件,新增修改 son 名字的 button 按钮;

1
2
3
4
5
6
7
8
9
10
11
12
13
import useFamilyStore, { updateSonName } from "@/store/familyStore";

// ...

const FamilyNames: FC = () => {
const names = useFamilyStore((state) => Object.values(state.family));
return (
<>
<h5>熊熊的名字:{names.join(", ")}</h5>
<button onClick={() => updateSonName("🎉")}>修改 son 的名字</button>
</>
);
};

基于 resetters 重置 familyStore

在 @/store/familyStore.ts 模块中导入 resetters,在调用 create() 时,向 resetters 中添加重置模块的函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
// #1
import resetters from "./tools/resetters";

const initFamilyState: FamilyStoreType = {
family: {
father: "Ifer",
mother: "Xxx",
son: "Yyy",
},
};

const useFamilyStore = create<FamilyStoreType>()(
immer(
devtools(
persist(
(set) => {
// #2
resetters.push(() => set(initFamilyState));
return {
...initFamilyState,
};
},
{
name: "family-store",
}
),
{
name: "family-store",
}
)
)
);

export default useFamilyStore;

export const updateSonName = (sonName: string) => {
useFamilyStore.setState((state) => {
state.family.son = sonName;
});
};

向 family 中添加 daughter 的名字

1. 修改 @/vite-env.dts 文件中的 FamilyType 类型定义,新增 daughter 属性;

1
2
3
4
5
6
7
8
type FamilyStoreType = {
family: {
father: string;
mother: string;
son: string;
daughter?: string;
};
};

2. 修改 @/store/familyStore.ts 模块,新增名为 addDaughterName 的 Action 函数;

1
2
3
4
5
6
7
// ...

export const addDaughterName = (daughterName: string) => {
useFamilyStore.setState((state) => {
state.family.daughter = daughterName;
});
};

3. 修改 @/components/family.tsx 模块,按需导入 addDaughterName 函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import useFamilyStore, {
updateSonName,
addDaughterName,
} from "@/store/familyStore";
// ...

const FamilyNames: FC = () => {
const names = useFamilyStore((state) => Object.values(state.family));
return (
<>
<h5>熊熊的名字:{names.join(", ")}</h5>
<button onClick={() => updateSonName("🎉")}>修改 son 的名字</button>
<button onClick={() => addDaughterName("sunny")}>添加 daughter</button>
</>
);
};

使用 useShallow 防止组件不必要的渲染

1. 在 FamilyMembers 组件中添加 useEffect 的调用,用来监视组件的 render 渲染,src/components/family.tsx;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const FamilyMembers: FC = () => {
// #mark
useEffect(() => {
console.log("触发了 FamilyMembers 组件的渲染");
});
const members = useFamilyStore((state) => Object.keys(state.family));
return (
<>
<h5>小熊一家的成员:</h5>
<p>{members.join(", ")}</p>
</>
);
};

const FamilyNames: FC = () => {
const names = useFamilyStore((state) => Object.values(state.family));
return (
<>
<h5>熊熊的名字:{names.join(", ")}</h5>
<button onClick={() => updateSonName("🎉")}>修改 son 的名字</button>
<button onClick={() => addDaughterName("sunny")}>添加 daughter</button>
</>
);
};

2. 此时,当组件首次渲染或更新渲染时,都会触发 useEffect 回调函数的执行。当我们点击 FamilyNames 组件中的修改 son 的名字按钮时,并没有为 family 对象添加任何新成员,但是却触发了 FamilyMembers 的更新渲染,这就导致了性能的浪费。此时,我们可以使用 useShallow 这个 zustand hook 帮助我们优化渲染性能(在更新前后,如果 selector 获取到的数据没有任何变化,则会防止组件的更新渲染,从而提升组件的渲染性能)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useShallow } from "zustand/react/shallow";

// ...

const FamilyMembers: FC = () => {
useEffect(() => {
console.log("触发了 FamilyMembers 组件的渲染");
});
// mark 把需要进行性能优化的 selector 包裹在 useShallow 中即可
const members = useFamilyStore(
useShallow((state) => Object.keys(state.family))
);
return (
<>
<h5>小熊一家的成员:{members.join(", ")}</h5>
</>
);
};

3. 修改完成后,再次点击修改名字按钮,发现不会导致 FamilyMembers 组件的更新渲染啦,因为更新前后的 members 数组没有任何变化。只有点击添加 daughter 按钮时,才会触发 FamilyMembers 组件的更新渲染,因为此时 member 数组的值从 [father,,mother,son] 变成了 [father,mother,son,daughter]。

订阅 Store 数据的变化

subscribe 的语法格式

在 zustand 中,使用 subscribe(fn) 可以用来订阅 Store 数据的变化,并在数据变化后执行 fn 回调函数。在回调函数中,接收两个形参 newValue 和 oldValue,其中:

  • newValue:表示变化后的新值

  • oldValue:表示变化前的旧值

同时,subscrible() 还返回一个取消订阅的函数。语法格式如下:

1
2
3
const unsubFn = useStore.subscribe((newValue, oldValue) => {
console.log(newValue, oldValue);
});

subscribe 的基本使用

1. 修改 @/components/family.tsx 中的 FamilyNames 组件,基于 subscribe() 订阅 familyStore 数据的变化;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...
const FamilyNames: FC = () => {
const names = useFamilyStore((state) => Object.values(state.family));
// #1
useEffect(() => {
const unsubFn = useFamilyStore.subscribe((newValue, oldValue) => {
console.log(newValue.family.son, oldValue.family.son);
});
// 清理函数,组件卸载时,取消订阅
return () => unsubFn();
}, []);

return (
<>
<h5>熊熊的名字:{names.join(", ")}</h5>
<button onClick={() => updateSonName("🎉")}>修改 son 的名字</button>
<button onClick={() => addDaughterName("sunny")}>添加 daughter</button>
</>
);
};

2. 点击按钮取消订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...

const FamilyNames: FC = () => {
// #1
const ref = useRef<() => void>();
const names = useFamilyStore((state) => Object.values(state.family));
useEffect(() => {
const unsubFn = useFamilyStore.subscribe((newValue, oldValue) => {
console.log(newValue.family.son, oldValue.family.son);
});
// #2
ref.current = unsubFn;
return () => unsubFn();
}, []);

return (
<>
<h5>熊熊的名字:{names.join(", ")}</h5>
<button onClick={() => updateSonName("🎉")}>修改 son 的名字</button>
<button onClick={() => addDaughterName("sunny")}>添加 daughter</button>
{/* #3 */}
<button onClick={() => ref.current && ref.current()}>取消订阅</button>
</>
);
};

🤠 subscribe 的缺点:只能订阅整个 Store 数据的变化,无法订阅 Store 下某个具体数据的变化。

使用 subscribeWithSelector 订阅 son 的变化

基于 subscribeWithSelector 这个中间件,可以订阅(监听)Store 中指定数据的变化。

1. 导入 subscribeWithSelector 中间件,并在创健 Store 的 Hook 是使用此中间件,src/store/familyStore.ts;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { create } from "zustand";
// #1
import { persist, devtools, subscribeWithSelector } from "zustand/middleware";

// ...

const useFamilyStore = create<FamilyStoreType>()(
devtools(
// #2
subscribeWithSelector(
immer(
persist(
(set) => {
resetters.push(() => set(initFamilyState));
return {
...initFamilyState,
};
},
{
name: "family-store",
}
)
)
),
{
name: "familyStore",
}
)
);

2. 调用 subscribe() 函数订阅具体数据的变化:src/components/family.tsx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const FamilyNames: FC = () => {
const ref = useRef<() => void>();
const names = useFamilyStore((state) => Object.values(state.family));
useEffect(() => {
// #mark
const unsubFn = useFamilyStore.subscribe(
(state) => state.family.son,
(newValue, oldValue) => {
console.log(oldValue, newValue, 233);
},
{ fireImmediately: true }
);
ref.current = unsubFn;
return () => unsubFn();
}, []);

return (
<>
<h5>熊熊的名字:{names.join(", ")}</h5>
<button onClick={() => updateSonName("🎉")}>修改 son 的名字</button>
<button onClick={() => addDaughterName("sunny")}>添加 daughter</button>
<button onClick={() => ref.current && ref.current()}>取消订阅</button>
</>
);
};

3. 其中 options 配置对象中的 firelmmediately: true 表示立即触发一次回调函数的执行。现在再测试,点击添加 daughter 不会再出发订阅了。

基于 subscribe 实现 Father 组件背景色的变换

需求:如果小鱼千的数量 ≥ 5,则让 Father 组件的背景色为 lightgreen;否则让 Father 组件的背景色为 lightgray。

1. 使用 subscribeWithSelector 中间件改造 fishStore.ts 模块;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
const useFishStore = create<FishStoreType>()(
devtools(
subscribeWithSelector(
immer(
persist(
(set) => {
resetters.push(() => set(initFishState));
return {
...initFishState,
};
},
{
name: "fish-store",
storage: createJSONStorage(() => sessionStorage),
}
)
)
),
{
name: "fishStore",
}
)
);
1
2
// ...
subscribeWithSelector(immer(devtools(persist())));

2. 改造 @/components/setup.tsx 模块中的 Father 组件,结合 useState、useEffect 和 zustand 的 subscribe,实现背景色变换的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ...
export const Father: FC = () => {
const brears = useBearStore((state) => state.bears);
// #1
const [bgColor, setBgColor] = useState<"lightgreen" | "lightgray">(
"lightgray"
);
// #2
useEffect(() => {
const unsubFn = useFishStore.subscribe(
(state) => state.fishes,
(newValue) => {
setBgColor(newValue >= 5 ? "lightgreen" : "lightgray");
},
{
fireImmediately: true,
}
);
return () => unsubFn();
}, []);
return (
// #3
<div style={{ padding: 10, borderRadius: 5, background: bgColor }}>
<h3>Father 组件</h3>
<p>Bears: {brears}</p>
<hr />
<Son />
</div>
);
};