Zustand

Zustand 使用说明

什么是 Zustand

Zustand 是一个小巧、快速且可扩展的状态管理解决方案,基于简化的 flux 原则 。它提供了基于 hooks 的舒适 API,没有样板代码,也不强制使用特定模式 。

安装

1
npm install zustand

基础使用

1. 创建 Store

使用 create 函数创建一个 store,它会返回一个 React Hook:

1
2
3
4
5
6
7
import { create } from 'zustand'

const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))

2. 在组件中使用

直接在任何组件中使用 hook,无需 Provider 包裹 :

1
2
3
4
5
6
7
8
9
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}

function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}

核心概念

选择状态切片

使用 selector 函数选择特定的状态,组件只会在选中的状态变化时重新渲染 :

1
2
const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)

异步 Actions

直接在 action 中使用 async/await,Zustand 不关心你的 action 是否异步 :

1
2
3
4
5
6
7
const useFishStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond)
set({ fishies: await response.json() })
},
}))

最佳实践

推荐的做法是将动作和状态放在存储中(让动作与状态位于同一位置)。

1
2
3
4
5
6
export const useBoundStore = create((set) => ({
count: 0,
text: 'hello',
inc: () => set((state) => ({ count: state.count + 1 })),
setText: (text) => set({ text }),
}))

这将创建一个包含数据和操作的自包含存储。

另一种方法是在模块级别定义操作,使其位于存储外部。

1
2
3
4
5
6
7
8
9
export const useBoundStore = create(() => ({
count: 0,
text: 'hello',
}))

export const inc = () =>
useBoundStore.setState((state) => ({ count: state.count + 1 }))

export const setText = (text) => useBoundStore.setState({ text })

这具有以下几个优势:

无需钩子即可调用操作
便于代码拆分
虽然这种模式没有明显缺点,但有些人可能更倾向于采用同位置放置模式,因为它具有封装特性

完整示例

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 { create } from "zustand";
import { devtools, persist, subscribeWithSelector } from "zustand/middleware";

const initialFoodValue = {
fish: 0,
mouse: 0,
};

export const useFoodStore = create<typeof initialFoodValue>()(
devtools(
subscribeWithSelector(
persist(() => initialFoodValue, { name: "food store" })
),
{ name: "food store" }
)
);

export const addOneFish = () =>
useFoodStore.setState((state) => ({ fish: state.fish + 1 }));

export const removeOneFish = () =>
useFoodStore.setState((state) => ({ fish: state.fish - 1 }));

export const removeAllFish = () => useFoodStore.setState({ fish: 0 });

immer -> devtools -> subscribeWithSelector -> persist 顺序

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { StateCreator, create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { createSelectors } from "../utils/createSelectors";
import { devtools, persist, subscribeWithSelector } from "zustand/middleware";

type TCatStoreState = {
cats: {
bigCats: number;
smallCats: number;
};
increaseBigCats: () => void;
increaseSmallCats: () => void;
summary: () => void;
};

const createCatSlice: StateCreator<
TCatStoreState,
[
["zustand/immer", never],
["zustand/devtools", unknown],
["zustand/subscribeWithSelector", never],
["zustand/persist", unknown]
]
> = (set, get) => ({
cats: {
bigCats: 0,
smallCats: 0,
},
increaseBigCats: () =>
set((state) => {
state.cats.bigCats++;
}),
increaseSmallCats: () =>
set((state) => {
state.cats.smallCats++;
}),
summary: () => {
const total = get().cats.bigCats + get().cats.smallCats;
return `There are ${total} cats in total. `;
},
});
export const useCatStore = createSelectors(
create<TCatStoreState>()(
immer(
devtools(
subscribeWithSelector(
persist(createCatSlice, {
name: "cat store",
})
),
{
enabled: true,
name: "cat store",
}
)
)
)
);

1. 使用 Slices 模式组织大型 Store

当 store 变得庞大时,可以将其分割成更小的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// fishSlice.js
export const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

// bearSlice.js
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
})

// 合并 slices
import { create } from 'zustand'
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))

2. 使用中间件增强功能

Persist 中间件 - 持久化状态

将状态保存到 localStorage 或其他存储引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

const useFishStore = create(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'food-storage',
storage: createJSONStorage(() => sessionStorage),
},
),
)

DevTools 中间件 - 调试

使用 Redux DevTools 调试状态变化:

1
2
3
4
5
import { devtools } from 'zustand/middleware'

const usePlainStore = create(devtools((set) => ({
// your store
})))

3. TypeScript 使用

使用 TypeScript 时,需要使用 create<State>()() 的双括号语法 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface BearState {
bears: number
increase: (by: number) => void
}

const useBearStore = create<BearState>()(
devtools(
persist(
(set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}),
{ name: 'bear-storage' },
),
),
)

4. React Context 集成

对于需要依赖注入或使用组件 props 初始化 store 的场景,推荐使用 vanilla store 配合 React Context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'

const store = createStore(...)
const StoreContext = createContext()

const App = () => (
<StoreContext.Provider value={store}>
...
</StoreContext.Provider>
)

const Component = () => {
const store = useContext(StoreContext)
const slice = useStore(store, selector)
...
}

5. 单一 Store 架构

推荐使用单一 store 架构,将全局状态集中在一个 store 中 。对于大型应用,使用 slices 模式来实现模块化

6. 性能优化

  • 使用 useShallow 进行浅比较,避免不必要的重渲染
  • 使用精确的 selector 只订阅需要的状态
  • 对于频繁变化的状态,考虑使用 subscribeWithSelector 中间件

Notes

Zustand 相比 Redux 的优势包括:更少的样板代码、不需要 Context Provider、基于 hooks 的 API、支持瞬态更新(不触发渲染)。相比 React Context,它提供了更少的样板代码、只在变化时渲染组件、集中式的基于 action 的状态管理。

更多详细文档可以访问官方文档

Wiki pages you might want to explore: