Zustand 状态管理

Zustand 状态管理

      • 安装
      • 创建 Store
      • 给 Store 添加TS类型约束
      • 在页面使用 Store
      • 返回 Store 中所有状态
      • 在 Store 中使用 async 异步方法
      • 使用 Immer Middleware (中间件) 更新深层嵌套的 State
      • 使用 get 方法,在 set 方法外访问 State 中的数据
      • 使用 selector
        • 什么是 selector ?
        • 为什么要使用 selector ?
        • 如何自动生成第一层的 selector ?
      • 使用 shallow 安全返回多个状态 selector
      • 使用 devtools 调试工具
        • 在生产环境关闭浏览器的状态调试工具
        • 给不同的 store 添加别名
      • 使用 persist 浏览器本地保存 State
        • 保存到 sessionStorage
        • partialize 设置本地存储保时只存部分状态
        • 排除 Store 中的某些状态
        • 清除 store 中的缓存
        • persist 在 middleware 里的顺序
      • 使用 subscribe 订阅关注
        • subscribe 在 Zustand 里是什么意思 ?
        • 为什么要使用 subscribe ?
        • 使用 subscribe
        • 使用 subscribeWithSelector
        • subscribeWithSelector 在 middleware 里的顺序
      • get/setState 在Store外控制 state
        • setState
        • getState
        • 使用 getState 用于初始化数据
      • 🚩 使用分离版本的 Actions,简化 Store
      • Typescript 建议
        • 从 store 中抽离 StateCreator

image.png

  • 使用 hook 的等方式创建和使用状态管理。
  • 可以直接使用 async 异步函数,而不需要像 Redux 一样,额外安装第三方插件才能实现。

安装

npm install zustand # or yarn add zustand or pnpm add zustand

创建 Store

import { create } from 'zustand'export const useBearStore = create((set) => ({bears: 0,user: {name: 'yi',age: 18},increasePopulation: () => set((state) => ({// ...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),setName: (name: string) => set((state) => ({...state,user: {...state.user,name}}))
}))

zustand 会自动合并第一层的 state,所以第一层可以不使用...state,但如果是更深层的状态,比如第二层或第三层,就还是需要 ...state 修改 state。

给 Store 添加TS类型约束

给 srore 定义类型

import { create } from 'zustand'type TBearStore = {bears: numberuser: {name: stringage: number}increasePopulation: () => voidremoveAllBears: () => voidsetName: (name: string) => void
}export const useBearStore = create<TBearStore>()((set) => ({bears: 0,user: {name: 'yi',age: 18},increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),setName: (name: string) => set((state) => ({...state,user: {...state.user,name}})),
}))

注意📢:给 store 添加 TS 类型时时,我们要在传入泛型的后面加一个()。具体原因感兴趣可以查看下面的连接 🔗 https://github.com/pmndrs/zustand/blob/main/docs/guides/typescript.md。

在页面使用 Store

Zustand 创建的 Store比较特别, 本质上就是一个hook,所以它能够很方便的被调用,你不需要像 Redux 或者 useContext 一样,外面还要包一层传送门。

import { useBearStore } from "@/stores/bearStore";export const BearBox = () => {const bears = useBearStore((state) => state.bears);const increasePopulation = useBearStore((state) => state.increasePopulation);const removeAllBears = useBearStore((state) => state.removeAllBears);return (<div className="box"><h1>Bear Box</h1><p>bears: {bears}</p><div><button onClick={increasePopulation}>add bear</button><button onClick={removeAllBears}>remove all bears</button></div></div>);
};

上面的 useBearStore,返回一个 clalback (回调函数),在这个回调函数里,可以获取到 state,这个state 就是 store 里所有的状态,然后你可以用它返回任何你在 useBearStore 中定义的 state 和 Action。

返回 Store 中所有状态

在上面使用 store 的示例中,我们可以看到,我们每次取出 store 中的数据都需要使用 const xx = useBearStore((state) => state.xx) 方式,如果你需要使用很多状态,每一个都这么写会很累,你可能会想,有没有什么方式更简便一点呢?

如果是需要返回 store中所有的状态,我们可以这么写:

import { useBearStore } from "@/stores/bearStore";export const BearBox = () => {const { bears, increasePopulation, removeAllBears } = useBearStore();return (<div className="box"><h1>Bear Box</h1><p>bears: {bears}</p><div><button onClick={increasePopulation}>add bear</button><button onClick={removeAllBears}>remove all bears</button></div></div>);
};

注意📢:如果你不需要全部状态,而是为了偷懒而这样写的话,可能会引起页面不必要的重复渲染,在小的 app 里无关紧要,但在大而复杂的项目里就会影响速度。

在 Store 中使用 async 异步方法

import { create } from 'zustand'type TBearStore = {user: {name: stringphone: string}getUserInfo: () => Promise<any>
}export const useBearStore = create<TBearStore>()((set) => ({user: {name: 'yi',phone: '13246566447'},getUserInfo: async () => {const res = await fetch('https://jsonplaceholder.typicode.com/users/1')const user = await res.json()set((state) => ({user: {...state.user,name: user.name}}))return user.name}
}))

使用 Immer Middleware (中间件) 更新深层嵌套的 State

import { create } from "zustand";
type TCatStoreState = {cats: {bigCats: number;smallCats: number;};increaseBigCats: () => void;increaseSmallCats: () => void;
};export const useCatStore = create<TCatStoreState>()((set, get) => ({cats: {bigCats: 0,smallCats: 0,},increaseBigCats: () => {set((state) => ({cats: {//zustand 只会自动合并第一层的 state, 所以这里要手动合并...state.cats,bigCats: state.cats.bigCats + 1,},}))},increaseSmallCats: () => {set((state) => ({cats: {...state.cats,smallCats: state.cats.smallCats + 1,},}))}})
)

在页面中使用

import { useCatStore } from "@/stores/catStore";export const CatBox = () => {const bigCats = useCatStore((state) => state.cats.bigCats);const smallCats = useCatStore((state) => state.cats.smallCats);const increaseBigCats = useCatStore((state) => state.increaseBigCats);const increaseSmallCats = useCatStore((state) => state.increaseSmallCats);return (<div className="box"><h1>Cat Box</h1><p>big cats: {bigCats}</p><p>small cats: {smallCats}</p><div><button onClick={increaseBigCats}>add big cats</button><button onClick={increaseSmallCats}>add small cats</button></div></div>)
}

在上面定义的 store 中,我们频繁的使用了 ...state 的方式来把 state 先复制一下,再定义新的值从而覆盖旧的 state 来更新 store 的状态。

我们可以使用 Immer Middleware 来解决这个问题:

  1. 首先安装 immer
pnpm i -D immer
  1. 使用方法也很简单,导入 immer ,然后在 create 方法中在在包裹一个 immer()
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";export const useCatStore = create(immer((set) => ({//...}))
)
  1. 示例:优化上面在 catStore.ts
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";type TCatStoreState = {cats: {bigCats: number;smallCats: number;};increaseBigCats: () => void;increaseSmallCats: () => void;
};export const useCatStore = create<TCatStoreState>()(immer((set) => ({cats: {bigCats: 0,smallCats: 0,},increaseBigCats: () =>set((state) => {state.cats.bigCats++;}),increaseSmallCats: () =>set((state) => {state.cats.smallCats++;}),}))
)

使用 immer 后我们直接通过函数的形式,使用 set 方法设置 state 中的值,在这个函数里不需要 return (不再需要返回一个对象)。

使用 get 方法,在 set 方法外访问 State 中的数据

在使用 zustand 时,是无法直接访问 state 中数据的,只能通过 zustand 给我们提供的 set 、 get 方法来访问 state状态。所以,如果需要再 set 方法外访问 state,那我们需要使用 get 方法。

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";type TCatStoreState = {cats: {bigCats: number;smallCats: number;};increaseBigCats: () => void;increaseSmallCats: () => void;summary: () => void;
};export const useCatStore = create<TCatStoreState>()(immer((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. `;}}))
)

使用的时候注意,summary 是一个函数,所以在使用的时候需要调用一下:

import { useCatStore } from "@/stores/catStore";export const CatBox = () => {const summary = useCatStore((state) => state.summary);console.log(summary())return (<div className="box"><h1>Cat Box</h1><p>small summary: {summary()}</p></div>)
}

使用 selector

什么是 selector ?

const bigCats = useCatStore((state) => state.cats.bigCats)
我们把 useCatStore 括号里的 (state) => state.cats.bigCats称为 selector,且这个 selector (选择器) 是一个回调函数。

为什么要使用 selector ?

在上面的实例中,我们在页面中消费 store 的时候,是非常繁琐的:

const bigCats = useCatStore((state) => state.cats.bigCats);
const smallCats = useCatStore((state) => state.cats.smallCats);
const increaseBigCats = useCatStore((state) => state.increaseBigCats);
const increaseSmallCats = useCatStore((state) => state.increaseSmallCats);
const summary = useCatStore((state) => state.summary);

如果我们要使用所有的状态,我们就可以直接从 useCatStore 解构出所有 state ,从而简化代码:

 const {cats: { bigCats, smallCats },increaseBigCats,increaseSmallCats,summary,} = useCatStore();

我们之前也提到过,这种方式 只适用与你需要使用全部状态,如果只是使用部分状态,那情况就不妙了,因为这会导致不必要的重渲染,接下来我们来看一个了 🌰:

假设我们右如下 CatBox、CatBox2 组件, 为了验证页面是否发生重渲染,我们给组件添加一个 Math.random()方法:

import { useCatStore } from "@/stores/catStore";export const CatBox = () => {const {cats: { bigCats, smallCats },increaseBigCats,increaseSmallCats,summary,} = useCatStore();console.log(summary());return (<div className="box"><h1>Cat Box</h1><p>big cats: {bigCats}</p><p>small cats: {smallCats}</p><p>{Math.random()}</p><div><button onClick={increaseBigCats}>add big cats</button><button onClick={increaseSmallCats}>add small cats</button></div></div>);
};
import { useCatStore } from "../stores/catStore";export const CatBox2 = () => {const { cats: { bigCats } } = useCatStore();return (<div className="box"><h1>Partial States from catStore</h1><p>big cats: {bigCats}</p><p>{Math.random()}</p></div>);
};
  • 点击左侧 CatBox 组件 add big cats 按钮时,两侧随机数发生变化了,这是正常的,因为在两个组件中我们都使用了 store 中的 bigCats;
  • 但是点击 add small cats 按钮时,两边又重新产生了随机数,说明组件重渲染了。

这就是问什么我们要使用 selector 来调用状态的原因,因为他可以避免页面不必要的重复渲染,我们更改一下 CatBox 组件中的代码:

import { useCatStore } from "@/stores/catStore";export const CatBox2 = () => {const bigCats = useCatStore((state) => state.cats.bigCats);return (<div className="box"><h1>Partial States from catStore</h1><p>big cats: {bigCats}</p><p>{Math.random()}</p></div>);
};

注意 ⚠️
上面的代码中,useCatStore()括号里放的就是我们上面说的 selector,它就是一个回调函数,这个回调函数会自动拿到一个 state,也就是 useCatStore中的全部 state 状态,然后我们按需返回所需要的状态,比如我们这里是 bigCats ,更改代码后,我们回到从新打开页面再次点击 add small cats 按钮时,右侧CatBox2 组件的随机数不再发生变化,说明使用 selector 方式可以避免页面重渲染。

但是,如果页面中需要使用 n 个状态,我们这么写,还是很拉胯的,先别急,其实作者在官方文档里,给我们提供了一个秘方,就是在第一层状态里,能大大提高你选择第一层状态时的效率。

如何自动生成第一层的 selector ?
  • Auto Generating Selectors

在项目新建 src/utils/createSelectors.ts 文件:

import { StoreApi, UseBoundStore } from 'zustand'type WithSelectors<S> = S extends { getState: () => infer T }? S & { use: { [K in keyof T]: () => T[K] } }: neverconst createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S,
) => {const store = _store as WithSelectors<typeof _store>store.use = {}for (const k of Object.keys(store.getState())) {;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])}return store
}

上面这段代码比较简单,其实就是把你的 store 作为输入值,然后把store 拓展成一个 use 的属性,这个 use 属性是一个对象,里面包含所有的 state 的 key,和它对应的 selector function,最后返回 store。

使用 createSelectors 方式也简单:

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { createSelectors } from "@/utils/createSelectors";type TCatStoreState = {cats: {bigCats: number;smallCats: number;};increaseBigCats: () => void;increaseSmallCats: () => void;summary: () => void;
};export const useCatStore = createSelectors(create<TCatStoreState>()(immer((set, get) => ({cats: {bigCats: 0,smallCats: 0,},increaseBigCats: () => set((state) => state.cats.bigCats++),increaseSmallCats: () => set((state) => state.cats.smallCats++)}))
))

在组件中使用:
image.png

import { shallow } from "zustand/shallow";
import { useCatStore } from "@/stores/useCatStore";export const CatController = () => {const increaseBigCats = useCatStore.use.increaseBigCats() const increaseSmallCats = useCatStore.use.increaseSmallCats()return (<div className="box"><h1>Cat Controller</h1><p>{Math.random()}</p><div><button onClick={increaseBigCats}>add big cats</button><button onClick={increaseSmallCats}>add small cats</button></div></div>);
}

使用 shallow 安全返回多个状态 selector

如果我们想避免组件重渲染的同时,还可以在 store 选择多个状态,那就要使用 shallow (平安符)。

  //   const { increaseBigCats, increaseSmallCats } = useCatStore();//   const increaseBigCats = useCatStore.use.increaseBigCats();//   const increaseSmallCats = useCatStore.use.increaseSmallCats();import { shallow } from "zustand/shallow";const { increaseBigCats, increaseSmallCats } = useCatStore((state) => ({increaseBigCats: state.increaseBigCats,increaseSmallCats: state.increaseSmallCats,}),shallow);
  • 这个 shallow 是一个判断函数,它判断第一层状态是否相等,需要从 zustand/shallow 中导入。
  • 为什么加入 shallow 函数后就可以避免重渲染问题呢,因为我们的上面的代码中我们使用 useCatStore 时返回的是一个 object ,它每一次都是重新产生的,而这个 shallow 函数的作用,就是用于比较两个 object 的第一层值是不是一样,如果一样,就认为相等,反之则不相等。如果你的情况更复杂,你还可以自己写这个 shallow 比较函数。

我们不仅仅可以返回一个对象,还可以返回一个数组:

import { shallow } from "zustand/shallow";const [increaseBigCats, increaseSmallCats] = useCatStore((state) => [state.increaseBigCats, state.increaseSmallCats],shallow
);

使用 devtools 调试工具

因为 zustand 和 Redux 是同门,所以可以直接借用 Redux 的调试工具来调试状态。

  • Redux DevTools
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'type TBearStore = {bears: numberincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()(devtools((set) => ({bears: 0,increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),})
))

image.png

如果触发没效果,需要配置一下 Redux tools ,将 instance 设置为 Autoselect instances:

image.png

在生产环境关闭浏览器的状态调试工具

如果你想设置在生产环境下关闭浏览器状态调试,可以使用 devtools 的第二个参数,这个参数是一个对象,我们在对象里加设置 enlabed 属性,值为布尔值,为 true 时会开启浏览器调试,反之则关闭。

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'type TBearStore = {bears: numberincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()(devtools((set) => ({bears: 0,increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),}),{enabled: true,}
))

如果使用了immer, 必须把 devtools 放在 immer 后面,因为 immer 可能会改变 state 状态:

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools } from "zustand/middleware";export const useCatStore = create(immer(devtools((set, get) => ({// ...})))
)
给不同的 store 添加别名

上面有提到我们因为不确定创建的 store 对应的 instance,导致调试时看不到状态变化, 所以将 instance 设置为 Autoselect instances,为了解决这个问题,我们在开启devtool时给store设置别名:

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'type TBearStore = {bears: numberincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()(devtools((set) => ({bears: 0,increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),}),{enabled: true,name: 'Bear Store',}
))

如下图,设置别名后,我们可以通过别名很方便的选择正确的 instance
image.png

使用 persist 浏览器本地保存 State

在很多时候,我们是需要将状态保存到本地的,常规的思路是使用手动将一些数据保存在浏览器的 localStorage 本地缓存中,但在 zustand 中提供了更简单的本地存储方法。

  • Zustand (persisting-store-data)

使用 persist 本地储存状态:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'type TBearStore = {bears: numberincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()(persist((set) => ({bears: 0,increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),}),{// 设置存储的key名称, 且必须是唯一的name: 'bear Store',}
))

image.png

保存到 sessionStorage

zustand 默认将开启 persist 的 store 全部保存到浏览器的 localStorage 中。

如果想存储到 sessionStorage:

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'export const useBearStore = create(persist((set, get) => ({bears: 0,addABear: () => set({ bears: get().bears + 1 }),}),{name: 'bear Store', // name of the item in the storage (must be unique)storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used},),
)
partialize 设置本地存储保时只存部分状态

🌰 比如有如下 store 代码:

export const useBoundStore = create(persist((set, get) => ({foo: 0,bar: 1,size: 24,userInfo: {name: 'yi',age: 25}}),),
)

我们只想将 userInfo 保存到本地存储

export const useBoundStore = create(persist((set, get) => ({foo: 0,bar: 1,size: 24,userInfo: {name: 'yi',age: 25}}),{name: 'bound Store'partialize: (state) => ({ userInfo: state.userInfo })},),
)
  • partialize 是一个回调函数,可以拿到所有 state,在这个函数内部需要返回一个对象,在这个对象里我们可以仅返回需要本地存储的字段。
排除 Store 中的某些状态
  • Zustand - partialize

比如我们想排除 foo、size 状态的本地缓存,可以这么写:

export const useBoundStore = create(persist((set, get) => ({foo: 0,bar: 1,size: 24,userInfo: {name: 'yi',age: 25}}),{name: 'bound Store'partialize: (state) =>Object.fromEntries(// 根据 key,过滤掉相应的 statesObject.entries(state).filter(([key]) => !['foo','size'].includes(key)),),},),
)
清除 store 中的缓存
import { create } from 'zustand'
import { persist } from 'zustand/middleware'type TBearStore = {bears: numberincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()(persist((set) => ({bears: 0,increasePopulation: () => set((state) => ({bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),}),{name: 'bear Store',}
))
import { useBearStore } from "@/stores/bearStore";export const BearBox = () => {const { bears, increasePopulation, removeAllBears } = useBearStore();return (<div className="box"><h1>Bear Box</h1><p>bears: {bears}</p><button onClick={increasePopulation}>add bear</button><div><button onClick={useBearStore.persist.clearStorage}>clear storage</button></div></div>);
}

注意 📢: ClearStorage 方法并不是 RestStorage ,上面这种方式可以正确清除浏览器中的 Storage,但是并没有清除 memory,所以,当你再次点击 add bear按钮时, bears的值不会从 0 开始自增,而是基于上次的 memory 值来改变的。

如果你要实现 rest states(重置状态),可以自己在 store 中定义一个重置逻辑

import { create } from 'zustand'
import { persist } from 'zustand/middleware'type TBearStore = {bears: numbercolor: stringsize: stringincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()(persist((set) => ({bears: 0,color: 'pink',size: 'big',increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 }),reset: () => set({bears: 0,color: 'pink',size: 'big',})}),{name: 'bear Store',}
))
persist 在 middleware 里的顺序

当同时使用 immer、devtools、persist 时,需要把 persist 放在 devtools 中间件里面, 注意 persist 中的第二个参数为必填项,必须设置 name 也就是本地存储时的 key 名称。

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools,persist } from "zustand/middleware";export const useCatStore = create(immer(devtools(persist((set, get) => ({// ...}),{name: 'user Store'})))
)
  • 关于 persist 更多的用法,点击参考链接 🔗

使用 subscribe 订阅关注

subscribe 在 Zustand 里是什么意思 ?

我们前面在组件中消费 store 中的数据时,都是这么写的:


const { increaseBigCats, increaseSmallCats } = useCatStore();
const increaseBigCats = useCatStore.use.increaseBigCats();
const increaseSmallCats = useCatStore.use.increaseSmallCats();// 或者
const [increaseBigCats, increaseSmallCats] = useCatStore((state) => [state.increaseBigCats, state.increaseSmallCats],shallow
)

上面的写法中, selector 返回的状态是 reactive 的,与 subscribe 不同的是, reactive 的状态会在每次状态变化后都重渲染。

举个生活中的例子,解释 reactive 和 subscribe 的区别比如你是一个 reactive 的小孩,那不管你是看到你妈妈在做饭还是做家务或者洗碗,你都会放下手上的游戏,跑过去看一下妈妈在做什么,有什么事是需要帮忙的。
而 subscribe 的小孩,不管妈妈在做洗碗、洗衣服、做饭,都不会有反应;但一旦看到妈妈两眼冒着火花盯着他的时候,他就会立刻放下手中的游戏,去帮妈妈晾衣服。
总结: subscribe 的小孩只会对某些特定情况做出反应,而 reactive 的小孩则会对所有情况做出反应。

为什么要使用 subscribe ?

比如熊需要食物,主食是鱼,所以我们可以创建一个 FoodStore ,里面包含一个 fish 状态,如果鱼的状态下降到 5 一些,我们就将页面背景变为红色,大于5的时候变为绿色。这里我们只需要关注 鱼的数量是否大于5 ,其它的状态我们并不想关注。
🌰 按照上面的思路,我们会这么定义 Store:

import { create } from 'zustand'type TBearStore = {bears: numbercolor: stringsize: stringincreasePopulation: () => voidremoveAllBears: () => void
}export const useBearStore = create<TBearStore>()((set) => ({bears: 0,color: 'pink',size: 'big',increasePopulation: () => set((state) => ({...state,bears: state.bears + 1})),removeAllBears: () => set({ bears: 0 })})
)type TFishStoreState = {fish: number;addOneFish: () => void;removeOneFish: () => void;removeAllFish: () => void;
}export const useFoodStore = create<TFishStoreState>((set) => ({fish: 0,addOneFish: () => set((state) => ({ fish: state.fish + 1 })),removeOneFish: () => set((state) => ({ fish: state.fish - 1 })),removeAllFish: () => set({ fish: 0 }),
}));

🌰 在页面中使用:

import { useBearStore, useFoodStore } from "@/stores/reactiveStore";export const BearBox = () => {const { bears, increasePopulation, removeAllBears } = useBearStore();const fish = useFoodStore((state) => state.fish);return (<div className="box" style={{ backgroundColor: fish > 5 ? 'lightgreen' : 'lightpink' }}><h1>Bear Box</h1><p>bears: {bears}</p><p>{Math.random()}</p><div><button onClick={increasePopulation}>add bear</button><button onClick={removeAllBears}>remove all bears</button></div></div>);
};export const FoodBox = () => {const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();return (<div className="box"><h1>Food Box</h1><p>fish: {fish}</p><div><button onClick={addOneFish}>🐟 + 1</button><button onClick={removeOneFish}>🐟 — 1</button><button onClick={removeAllFish}>Remove all fish 🗑️ </button></div></div>);
};

可以看到当我们点击右侧 Food Box 组件的按钮时,无论是添加鱼的数量还是减少鱼的数量,左侧 Bear Box组件都会重渲染,虽然这是正常的(因为两个组件我们都用到了fish状态),但是如果是更复杂的程序,页面频繁的点击操作,每次都要重新渲染页面,就会影响性能。

使用 subscribe

使用 subscribe 可以订阅全局状态并监听状态变化, 而不需要重渲染。

🌰 我们修改一下 BearBox 组件代码:

export const BearBox = () => {const { bears, increasePopulation, removeAllBears } = useBearStore();// const fish = useFoodStore((state) => state.fish);const [bgColor, setBgColor] = useState('lightpink');useEffect(() => {// subscribe 返回一个 unsubscribe 函数,我们可以通过变量接收const unsub = useFoodStore.subscribe((state, prevState) => {if (prevState.fish <= 5 && state.fish > 5) {setBgColor("lightgreen");} else if (prevState.fish > 5 && state.fish <= 5) {setBgColor("lightpink");}})// 返回 unsub,即可实现页面销毁的同时也销毁 subscribe 订阅return unsub;}, []);return (<div className="box" style={{ backgroundColor: bgColor }}><h1>Bear Box</h1><p>bears: {bears}</p><p>{Math.random()}</p><div><button onClick={increasePopulation}>add bear</button><button onClick={removeAllBears}>remove all bears</button><button onClick={useBearStore.persist.clearStorage}>clear storage</button></div></div>);
};

image.png
subscribe方法解释:

  • subscribe方法返回一个 listener (监听器) 回调函数,可以拿到 state,和 prevState (上一次的状态)参数。
  • 在这个回调函数里写的代码逻辑,在 每次state发生变化时,都会重新执行,但不会引起页面重渲染。
  • subscribe 可以放在组件内,也可以放在组件外面。注意:如果要放在组件里面时,我们要尽量把它放在 useEffect 🪝中,subscribe 会返回一个 unSubscribe 的方法,我们可以通过一个变量接收,然后在 useEffect 中 return,即可实现在组件销毁(页面隐藏)时卸载订阅。
使用 subscribeWithSelector

假如你有很多状态,但只关心其中的一部分,那我们还可以使用 subscribeWithSelector 中间件来 subscribe 一部分状态。

type TFishStoreState = {fish: number;addOneFish: () => void;removeOneFish: () => void;removeAllFish: () => void;
}export const useFoodStore = create<TFishStoreState>()(subscribeWithSelector((set) => ({fish: 0,addOneFish: () => set((state) => ({ fish: state.fish + 1 })),removeOneFish: () => set((state) => ({ fish: state.fish - 1 })),removeAllFish: () => set({ fish: 0 }),}))
)

🌰 在组件中使用:

import { useBearStore, useFoodStore } from "@/stores/subscribeStore";
import { useEffect, useState } from "react";
import { shallow } from "zustand/shallow";export const BearBox = () => {const { bears, increasePopulation, removeAllBears } = useBearStore();const [bgColor, setBgColor] = useState('lightpink');useEffect(() => {const unsub = useFoodStore.subscribe((state) => state.fish,(fish, prevFish) => {if (prevFish <= 5 && fish > 5) {setBgColor("lightgreen");} else if (prevFish > 5 && fish <= 5) {setBgColor("lightpink");}},{equalityFn: shallow, // 判断两个对象是否相等fireImmediately: true, // 是否在第一次调用(初始化时)立刻执行});return unsub;}, []);return (<div className="box" style={{ backgroundColor: bgColor }}><h1>Bear Box</h1><p>bears: {bears}</p><p>{Math.random()}</p><div><button onClick={increasePopulation}>add bear</button><button onClick={removeAllBears}>remove all bears</button></div></div>);
};

image.png
开启 subscribeWithSelector 中间件后,store 中的 subscribe 方法和之前就不同的,这个 subscribe 会返回三个参数,selector、listener、还有一个 options 配置对象。注意 listener 回调函数可以获取的参数也不同,拿到的是你选择的 selectedState -> selector (state.fish) ,还有 previousState (上一次的 state.fish)。

subscribeWithSelector 在 middleware 里的顺序

subscribeWithSelector 要放在 devtools 和 persist 中间件的中间:

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools,persist, subscribeWithSelector } from "zustand/middleware";export const useCatStore = create(immer(devtools(subscribeWithSelector(persist((set, get) => ({// ...}),{name: 'user Store'}))))
)

get/setState 在Store外控制 state

使用 getState、setState 方法可以在组件或独立的 JS文件中操作 store 中的状态。

setState

🌰 有下面 store 代码:

type TFishStoreState = {fish: number;addOneFish: () => void;removeOneFish: () => void;removeAllFish: () => void;
}export const useFoodStore = create<TFishStoreState>((set) => ({fish: 0,addOneFish: () => set((state) => ({ fish: state.fish + 1 })),removeOneFish: () => set((state) => ({ fish: state.fish - 1 })),removeAllFish: () => set({ fish: 0 }),
}));

我们的组件代码如下,导出并使用了所有状态,但是我还想在不修改 useFoodStore.ts 文件的情况下,再添加一个方法,比如在页面中增加一个按钮,每次点击时,让 fish 状态的值 +5:

import { useFoodStore } from "@/stores/foodStore";export const FoodBox = () => {const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();return (<div className="box"><h1>Food Box</h1><p>fish: {fish}</p><div><button onClick={addOneFish}>add one fish</button><button onClick={removeOneFish}>remove one fish</button><button onClick={removeAllFish}>remove all fish</button></div></div>);
};

我们可以使用 setState 方法,手动添加一个 Action


import { useFoodStore } from "@/stores/foodStore";export const FoodBox = () => {const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();const add5Fish = () => {useFoodStore.setState((state) => ({fish: state.fish + 5,}));};return (<div className="box"><h1>Food Box</h1><p>fish: {fish}</p><div><button onClick={addOneFish}>add one fish</button><button onClick={removeOneFish}>remove one fish</button><button onClick={removeAllFish}>remove all fish</button><button onClick={add5Fish}>add 5 fish</button></div></div>);
};
getState

getState用于在 store 外面获取状态,但它是 non-reactive 的,什么意思呢 ?

const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();const fish = useFoodStore((state) => state.fish);

上面两种消费 store 的方式,那就是 reactive 的。( reactive 的状态会在每次状态变化后都重渲染,而 subscribe 只会在开启订阅的状态发生变化时重渲染 )。

const fish = useFoodStore.getState().fish; // non-reactive

上面代码我们使用 getState 获取 fish 状态,即使当 store 中的 fish 在其它地方发生了改变,组件也不会重渲染,所以上面的 fish 也不知道,store 中的状态发生变化了。

既然页面不更新的话,那 getState 能用来干嘛呢 ?

使用 getState 用于初始化数据

在之前的讲到使用 subscribeWithSelector 🌰 中,我们手动给 bgColor 设置了初始值,我们还可以使用 getState 从状态里获取初始值:

type TBGColor = "lightgreen" | "lightpink" | undefinedexport const BearBox = () => {const { bears, increasePopulation, removeAllBears } = useBearStore();const [bgColor, setBgColor] = useState<TBGColor>(() => {return useFoodStore.getState().fish > 5 ? "lightgreen" : "lightpink"});useEffect(() => {const unsub = useFoodStore.subscribe((state) => state.fish,(fish, prevFish) => {if (prevFish <= 5 && fish > 5) {setBgColor("lightgreen");} else if (prevFish > 5 && fish <= 5) {setBgColor("lightpink");}},{equalityFn: shallow,fireImmediately: true,});return unsub;}, []);return (<div className="box" style={{ backgroundColor: bgColor }}><h1>Bear Box</h1><p>bears: {bears}</p><p>{Math.random()}</p><div><button onClick={increasePopulation}>add bear</button><button onClick={removeAllBears}>remove all bears</button></div></div>);
};

🚩 使用分离版本的 Actions,简化 Store

  • Zustand |practice-with-no-store-actions

再来回顾一下我们之前定义store 时的写法:

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools, subscribeWithSelector, persist } from "zustand/middleware";type TFishStoreState = {fish: number;addOneFish: () => void;removeOneFish: () => void;removeAllFish: () => void;
}export const useFoodStore = create<TFishStoreState>()(immer(devtools(subscribeWithSelector(persist((set) => ({fish: 0,addOneFish: () => {set((state) => ({ fish: state.fish + 1 }))},removeOneFish: () => {set((state) => ({ fish: state.fish - 1 }))},removeAllFish: () => {set({ fish: 0 });},}),{name: "food store",})),{name: "food store"}))
);

是不是有点回调地狱的感觉了,一层又包含一层, state 和 Action 都在一起,而且在页面使用的时候,我们还得写各种 const xx = useFoodStore(selector) 如果开发中都这么写,我想你和我一样肯定会骂人,甚至不会考虑这个状态管理库。先别急,我们试着把代码重写一下:

  1. 先剪切所有 Action 方法的代码

image.png

  1. 修改一下 Action 导出成方法:

image.png

  1. 将报错的 set 替换成 useFoodStore.setState

image.png

  1. 我们还可以将 state 提取出来,最后完成代码如下
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools, subscribeWithSelector, persist } from "zustand/middleware";const initialState = {fish: 0
}export const useFoodStore = create<typeof initialState>()(immer(devtools(subscribeWithSelector(persist(() => initialState, { 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 });
}

在页面中使用的时候,导入对应的 Action 就可以了。


import {useFoodStore,addOneFish,removeOneFish,removeAllFish,
} from "@/stores/foodStore";export const FoodBox = () => {const fish = useFoodStore((state) => state.fish)return (<div className="box"><h1>Food Box</h1><p>fish: {fish}</p><div><button onClick={ addOneFish }>add one fish</button><button onClick={ removeOneFish }>remove one fish</button><button onClick={ removeAllFish }>remove all fish</button></div></div>)
}
  • 不再需要使用 hook 来调用 store 中的 Action。
  • 可以更灵活的分离、组织代码。
  • 并且不会存在任何负面效果 (指之前提到一系列的重渲染问题 🙋)

Typescript 建议

从 store 中抽离 StateCreator

来看看下面的代码,我们的 store 有很多的状态和 Action,我们把所有中间件还有状态都写在了一起,非常拥挤:

import { 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;
};export const useCatStore = createSelectors(create<TCatStoreState>()(immer(devtools(subscribeWithSelector(persist((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. `;},}),{ name: "cat store" }),),{ name: "cat store" })))
)

我们可以把 StateCreator 从 store 中提取出来

import { type 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;
};type TMiddlewares = [["zustand/immer", never],["zustand/devtools", unknown],["zustand/subscribeWithSelector", never],["zustand/persist", unknown]
]const createCatSlice: StateCreator<TCatStoreState, TMiddlewares> = (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. 使用Ctrl + Shift + -> + -> + -> 选中括号内所有StateCreator数据
  2. 然后右键菜单选择 Refactor (重构)
  3. 在弹出的菜单选择 extract to constant in enclosing scope (提取到封闭范围中的 constant)
  4. 然后给提取出的 StateCreator 取一个变量名为 createCatSlice
  5. 给 createCatSlice 定义类型,从 zustand 中导入 StateCreator 类型,StateCreator 需要传入两个范型,第一个是我们定义 Store 的TS类型,第二个是中间件的TS类型。

参考链接 🔗:

  • Zustand | TypeScript 指南

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.tpcf.cn/news/611369.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GNN如何处理表格?

链接: https://ieeexplore.ieee.org/document/10184514 在这篇综述中&#xff0c;我们深入探讨了使用图神经网络&#xff08;GNNs&#xff09;进行表格数据学习&#xff08;TDL&#xff09;的领域&#xff0c;这是一个深度学习方法在分类和回归任务中相比传统方法表现出越来越…

Unity中BRP下的深度图

文章目录 前言一、在Shader中使用1、在使用深度图前申明2、在片元着色器中 二、在C#脚本中开启摄像机深度图三、最终效果 前言 在之前的文章中&#xff0c;我们实现了URP下的深度图使用。 Unity中URP下使用屏幕坐标采样深度图 在这篇文章中&#xff0c;我们来看一下BRP下深度…

2024-01-03 无重叠区间

435. 无重叠区间 思路&#xff1a;和最少数量引爆气球的箭的思路基本都是一致了&#xff01;贪心就是比较左边的值是否大于下一个右边的值 class Solution:def eraseOverlapIntervals(self, points: List[List[int]]) -> int:points.sort(keylambda x: (x[0], x[1]))# 比较…

2023-12-30 买卖股票的最佳时机 II和跳跃游戏以及跳跃游戏 II

122. 买卖股票的最佳时机 II 思路&#xff1a;关键点是每一次利用峰值来计算【画图好理解一点&#xff0c;就是计算陡坡的值】&#xff01;每一次累加和的最大! 或者可以这样理解&#xff0c;把利润划分为每天的&#xff0c;如假如第 0 天买入&#xff0c;第 3 天卖出&#xf…

Spring MVC 的controller方法返回值

controller方法返回值 返回ModelAndView 说明&#xff1a;controller方法中定义ModelAndView对象并返回&#xff0c;对象中可添加model数据、指定view 返回字符串 逻辑视图名 说明&#xff1a;controller方法返回字符串可以指定逻辑视图名&#xff0c;通过视图解析器解析为…

2024年最牛家用NAS+虚拟化方案,极低功耗

谈谈个人搭建NAS服务器有哪些需求 我有5台华为服务器、群晖、DX4600、路由器、交换机、小主机等&#xff0c;为了尝试出最牛NAS方案&#xff0c;前后投入了几万元&#xff0c;可谓是发烧友。 听我劝&#xff0c;照着这个方案执行&#xff0c;爽死你。 低功耗。NAS是长期运行的…

golang实现skiplist 跳表

跳表 package mainimport ("errors""math""math/rand" )func main() {// 双向链表///**先理解查找过程Level 3: 1 6Level 2: 1 3 6Level 1: 1 2 3 4 6比如 查找2 ; 从高层往下找;如果查找的值比当前值小 说明没有可查找的值2比1大 往当前…

window的两种监听方式区别

一、window.onmessage window.onmessage 是一个属性&#xff0c;直接赋值为事件处理函数。当使用这个方式时&#xff0c;只能绑定一个事件处理函数&#xff0c;如果后续再次赋值&#xff0c;会覆盖之前的处理函数。如果在同一个页面中只有一个地方需要处理消息&#xff0c;这种…

ELF文件格式解析二

使用objdump命令查看elf文件 objdump -x 查看elf文件所有头部的信息 所有的elf文件。 程序头部&#xff08;Program Header&#xff09;中&#xff0c;都以 PT_PHDR和PT_INTERP先开始。这两个段必须在所有可加载段项目的前面。 从上图中的INTERP段中&#xff0c;可以看到改段…

《GreenPlum系列》GreenPlum详细入门教程02-GreenPlum安装

文章目录 第二章 GreenPlum安装1.Docker创建centos容器1.1 拉取centos7镜像1.2 创建容器1.3 进入容器1.4 容器和服务器免密操作1.4.1 生成密钥1.4.2 拷贝密钥 1.5 安装ssh服务和网络必须应用1.6 容器设置root密码1.6.1 安装passwd应用1.6.2 容器本机root设置密码 1.7 容器本机免…

leetcode双指针问题总结 Python

目录 1. 二分查找 2. 有序数组中寻找两个数和等于某数 3. 两数平方和 4. 翻转字符串中的元音字符 5. 判断是否为回文字符串&#xff08;最多可以删除一个字符&#xff09; 6. 归并两个有序数组 7. 判断链表是否有环 8. 最长子序列 1. 二分查找 从有序数组 nums 中查找…

uniapp获取手机当前信息及应用版本

appVersion 是app端查询的数据信息 appWgtVersion 是浏览器端查询的数据信息 onLoad() {const systemInfo uni.getSystemInfoSync();console.log(systemInfo);// #ifdef H5const uniAppVersion systemInfo.appVersion;// #endif// #ifndef H5const uniAppVersion systemIn…

C++学习笔记——对象的指针

目录 一、对象的指针 二、减少对象的复制开销 三、应用案例 游戏引擎 图像处理库 数据库管理系统 航空航天软件 金融交易系统 四、代码的案例应用 一、对象的指针 是一种常用的技术&#xff0c;用于处理对象的动态分配和管理。使用对象的指针可以实现以下几个方面的功…

无法访问Bing网站 - 解决方案

问题 Bing官方网址&#xff1a;https://www.bing.com/ 电脑无法访问Bing网站&#xff0c;但手机等移动设备可以访问Bing网站&#xff0c;此时可尝试以下方案。 以下方案适用于各种系统&#xff0c;如Win/Linux系统。 解决方案 方案1 修改Bing网址为&#xff1a;https://www4…

JAVA毕业设计632—基于Java+ssm的宠物店商城系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于Javassm的宠物店商城系统(源代码数据库)632 一、系统介绍 本项目分为用户、营养师、管理员三种角色 1、用户&#xff1a; 登录、注册、宠物信息、宠物粮食、宠物用品、宠物疫…

二叉树基础oj练习(单值二叉树、相同的树、二叉树的前序遍历)

讲了这么多数据结构相关的知识(可以看我的数据结构文章专栏): 抓紧刷题巩固一下了 目录 1.单值二叉树 题目描述 思路1 代码1 思路2 代码2 2.相同的树 题目描述 思路 代码 3.二叉树的前序遍历 代码 思路 1.单值二叉树 965. 单值二叉树 - 力扣&#xff08;LeetCod…

C#进行Web API开发时,遇到的常见问题

当使用C#进行Web API开发时&#xff0c;可能会遇到以下一些常见问题&#xff1a; 跨域请求 由于浏览器的同源策略限制&#xff0c;跨域请求可能会引发问题。解决方法可以使用CORS&#xff08;跨域资源共享&#xff09;来允许从特定的域名或端口访问你的API。 // 添加CORS中间…

influx v2 cli设置influxdb过期策略

目录 背景解法设置config设置buckets找到需要修改的bucket ID更新bucket保留策略 参考资料 背景 最近刚使用influxdb&#xff0c;需要对某个db设置一个过期策略。由于初次使用&#xff0c;对influx client命令行不熟悉&#xff0c;于是在网上找相关的教程&#xff0c;但是搜到…

pycharm社区版配置flask开发环境

新建配置文件&#xff0c;类型选择Shell Script 设置Execute中flask.exe的路径 设置options &#xff1a;--appflask_app.py run --port5000 --debug 设置working 路径 设置环境变量FLASK_APPflask_app.py;FLASK_ENVdevelopment 注意&#xff1a;FLASK_APPflask_app.py和上…

YOLOv8 损失函数改进 | 引入 Shape-IoU 考虑边框形状与尺度的度量

🗝️改进YOLOv8注意力系列一:结合ACmix、Biformer、BAM注意力机制 论文讲解加入代码本文提供了改进 YOLOv8注意力系列包含不同的注意力机制以及多种加入方式,在本文中具有完整的代码和包含多种更有效加入YOLOv8中的yaml结构,读者可以获取到注意力加入的代码和使用经验,总…