Zustand 状态管理

Zustand 状态管理

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

安装

bash 复制代码
npm install zustand # or yarn add zustand or pnpm add zustand

创建 Store

typescript 复制代码
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 定义类型

typescript 复制代码
import { create } from 'zustand'

type TBearStore = {
  bears: number
  user: {
    name: string
    age: number
  }
  increasePopulation: () => void
  removeAllBears: () => void
  setName: (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 一样,外面还要包一层传送门。

typescript 复制代码
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中所有的状态,我们可以这么写:

typescript 复制代码
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 异步方法

typescript 复制代码
import { create } from 'zustand'

type TBearStore = {
  user: {
    name: string
    phone: 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

typescript 复制代码
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,
        },
      }))
    }
  })
)

在页面中使用

typescript 复制代码
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
typescript 复制代码
pnpm i -D immer
  1. 使用方法也很简单,导入 immer ,然后在 create 方法中在在包裹一个 immer()
typescript 复制代码
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

export const useCatStore = create(
  immer(
    (set) => ({
      //...
    })
  )
)
  1. 示例:优化上面在 catStore.ts
typescript 复制代码
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 方法。

typescript 复制代码
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 是一个函数,所以在使用的时候需要调用一下:

typescript 复制代码
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 的时候,是非常繁琐的:

typescript 复制代码
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 ,从而简化代码:

typescript 复制代码
 const {
    cats: { bigCats, smallCats },
    increaseBigCats,
    increaseSmallCats,
    summary,
  } = useCatStore();

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

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

typescript 复制代码
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>
  );
};
typescript 复制代码
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 组件中的代码:

typescript 复制代码
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 ?

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

typescript 复制代码
import { StoreApi, UseBoundStore } from 'zustand'

type WithSelectors<S> = S extends { getState: () => infer T }
  ? S & { use: { [K in keyof T]: () => T[K] } }
  : never

const 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 方式也简单:

typescript 复制代码
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++)
    })
  )
))

在组件中使用:

typescript 复制代码
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 (平安符)。

typescript 复制代码
  //   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 比较函数。

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

typescript 复制代码
import { shallow } from "zustand/shallow";

const [increaseBigCats, increaseSmallCats] = useCatStore(
    (state) => [state.increaseBigCats, state.increaseSmallCats],
    shallow
);

使用 devtools 调试工具

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

typescript 复制代码
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

type TBearStore = {
  bears: number
  increasePopulation: () => void
  removeAllBears: () => void
}

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

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

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

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

typescript 复制代码
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

type TBearStore = {
  bears: number
  increasePopulation: () => void
  removeAllBears: () => 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 状态:

typescript 复制代码
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设置别名:

typescript 复制代码
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

type TBearStore = {
  bears: number
  increasePopulation: () => void
  removeAllBears: () => 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

使用 persist 浏览器本地保存 State

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

使用 persist 本地储存状态:

typescript 复制代码
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type TBearStore = {
  bears: number
  increasePopulation: () => void
  removeAllBears: () => 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',
  }
))
保存到 sessionStorage

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

如果想存储到 sessionStorage:

typescript 复制代码
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 代码:

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

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

typescript 复制代码
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 中的某些状态

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

typescript 复制代码
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,过滤掉相应的 states
          Object.entries(state).filter(([key]) => !['foo','size'].includes(key)),
        ),
    },
  ),
)
清除 store 中的缓存
typescript 复制代码
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type TBearStore = {
  bears: number
  increasePopulation: () => void
  removeAllBears: () => void
}

export const useBearStore = create<TBearStore>()(persist(
  (set) => ({
    bears: 0,
    increasePopulation: () => set((state) => ({
      bears: state.bears + 1
    })),
    removeAllBears: () => set({ bears: 0 }),
  }),
  {
    name: 'bear Store',
  }
))
typescript 复制代码
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 中定义一个重置逻辑

typescript 复制代码
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type TBearStore = {
  bears: number
  color: string
  size: string
  increasePopulation: () => void
  removeAllBears: () => 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 名称。

typescript 复制代码
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'
        }
      )
    )
  )
)

使用 subscribe 订阅关注

subscribe 在 Zustand 里是什么意思 ?

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

typescript 复制代码
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:

typescript 复制代码
import { create } from 'zustand'

type TBearStore = {
  bears: number
  color: string
  size: string
  increasePopulation: () => void
  removeAllBears: () => 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 }),
}));

🌰 在页面中使用:

typescript 复制代码
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 组件代码:

typescript 复制代码
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>
  );
};


subscribe方法解释:

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

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

typescript 复制代码
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 }),
    })
  )
)

🌰 在组件中使用:

typescript 复制代码
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>
  );
};

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

subscribeWithSelector 在 middleware 里的顺序

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

typescript 复制代码
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 代码:

typescript 复制代码
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:

typescript 复制代码
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

typescript 复制代码
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 的,什么意思呢 ?

typescript 复制代码
const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();

const fish = useFoodStore((state) => state.fish);

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

typescript 复制代码
const fish = useFoodStore.getState().fish; // non-reactive

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

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

使用 getState 用于初始化数据

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

typescript 复制代码
type TBGColor = "lightgreen" | "lightpink" | undefined

export 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

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

typescript 复制代码
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 方法的代码
  1. 修改一下 Action 导出成方法:
  1. 将报错的 set 替换成 useFoodStore.setState
  1. 我们还可以将 state 提取出来,最后完成代码如下
typescript 复制代码
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 就可以了。

typescript 复制代码
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,我们把所有中间件还有状态都写在了一起,非常拥挤:

typescript 复制代码
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 中提取出来

typescript 复制代码
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类型。

参考链接 🔗:

相关推荐
计算机魔术师26 分钟前
Ethan Mollick 谈点与群:智能体自组织为何让管理假设失效
前端
故七月1 小时前
GEO 信源评估体系:如何判断一条网页信源能否被大模型采信
前端·网络·人工智能
liangshanbo12151 小时前
Webpack 常见 Plugin 面试题
前端·webpack·node.js
xxwl5851 小时前
vue3的入门学习
前端·vue.js·学习
用户847172102841 小时前
.shot 文件格式规范
前端·后端
web打印社区1 小时前
政务办事大厅:叫号小票、办理回执网页怎么静默出纸
前端·javascript·vue.js·pdf·html·政务
C++ 老炮儿的技术栈2 小时前
有符号变量与无符号变量的区别
服务器·开发语言·前端·数据结构·c++·算法·c
aixingpan2 小时前
aixingpan.cn API开发文档:api_docs_bichart_natalvssecondary2接口指南
前端·php
liangshanbo12152 小时前
Webpack Tree Shaking 面试题整理
前端·webpack·node.js
去伪存真2 小时前
从乱码到高精度检索:探矿业务中 TXT、Word、PDF 与网页的 RAG 清洗之道
前端·agent