zustand快速上手(TypeScript)

注意:本文章基于zustand v5.0.7

介绍

Zustand是一个轻量便捷的全局状态管理库,可在短时间内快速上手

对比

维度 Redux MobX Zustand
样板代码 多(action、reducer、selector) 少(装饰器 / makeAutoObservable) 极少(create(set => …) 即可)
异步 需中间件(thunk/saga) 原生 runInAction / flow 原生 async/await
绑定组件 react-redux connect/useSelector observer 高阶组件 直接 useStore 钩子

快速上手

安装

1
2
3
npm i zustand
# npm i immer persist
# 可能需要用到的中间件依赖

创建状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface UserStore {
id: number
age: number
arr: number[]
setId: (id: number) => void
ageAdd1: () => void
addNum: (n: number) => void
}

const useUserStore = create<UserStore>()(
(set, get) => ({
id: 0,
age: 0,
arr: [],
setId: (id) => set({id: id}),
ageAdd1: () => set(state => ({age: state.age+1})),
// ageAdd1: () => set({age: get().age+1})
addNum: (n) => set(state => ({arr: [...state.arr, n]}))
})
)

注意:必须返回新的对象引用,否则会导致组件不刷新,ts下建议使用create<>()()而不是create<>()

引用状态

1
2
3
4
5
6
7
8
9
10
const ele:React.FC = () => {
const age = useUserStore(state => state.age)
const ageAdd1 = useUserStore(state => state.ageAdd1)
return (
<div>
age: {age}
<button onClick={ageAdd1}/>
</div>
)
}

状态选择

不建议使用useUserStore()直接获取整个状态对象,这会导致状态中任意属性变化都触发组件重新渲染,建议使用Selector选择状态属性:

1
2
const id = useUserStore(state => state.id)
const name = useUserStore(state => state.name)

每次状态变化时zustand会重新执行Selector并进行引用比较,若Selector返回值的引用变化则重新渲染相应的组件

因为zustand使用引用比较判断变化,所以直接使用结构赋值会导致State无限循环:

1
2
3
const [id, name] = useUserStore(state => [id, name])
const {id, name} = useUserStore(state => ({id: state.id, name: state.id}))
//❌,每次渲染都会返回新的数组/对象,这会导致State无限循环

若需要获取多个状态属性,应该使用useShallow:(zustand v5)

1
2
3
const [id, name] = useUserStore(useShallow(state => [id, name]))
const {id, name} = useUserStore(useShallow(state => ({id: state.id, name: state.id})))
//✅

shallow会对对象进行浅层属性比较以避免无限循环

使用immer中间件实现可变数据结构

安装immer

1
npm i immer

使用immer中间件

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 { immer } from 'zustand/middleware/immer'

type State = {
count: number
}

type Actions = {
increment: (qty: number) => void
decrement: (qty: number) => void
}

export const useCountStore = create<State & Actions>()(
immer((set) => ({
count: 0,
increment: (qty: number) =>
set((state) => {
state.count += qty
}),
decrement: (qty: number) =>
set((state) => {
state.count -= qty
}),
})),
)

使用immer后修改状态不再需要返回新的state对象

使用persist中间件实现状态持久化

安装persist

1
npm i persist

使用persist

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, createJSONStorage } from 'zustand/middleware'

interface FishStore {
fishes: number,
addAfish: () => void
}

const useFishStore = create<FishStore>()(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'food-storage', // unique name
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
}
)
)

可以使用sessionStorange,localStorage(默认),cookie,indexeddb等持久化,只需在createJSONStrange中能够返回存储引擎的函数(存储引擎应具有getItem,setItem和removeItem方法)

其他问题

为什么TS下create()需要柯林化create<>()()

这是对microsoft/TypeScript#10571的变通方案
对于

1
2
3
4
5
6
7
function case3<A, B, C>(b: string, c: boolean): A {}

// incorrect type of A - left unspecified:
example('thing');

// correct, but unnecessarily verbose - we already know that 'thing' is a string and true is a bool
example<number, string, boolean>('thing', true);

可以变通写成

1
2
3
function case4<A>(): <B, C>(b:string, c:boolean) => A{}

example<number>()("thing", true)

case4现在什么都不做,只会返回一个类型为<B, C>(b:string, c:boolean) => A{}的函数,实现了A手动指定而B,C自动推断

zustand中create的定义:

1
2
3
4
5
type Create = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, [], Mos>): UseBoundStore<Mutate<StoreApi<T>, Mos>>;
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, [], Mos>) => UseBoundStore<Mutate<StoreApi<T>, Mos>>;
};
export declare const create: Create;