Zustand 使用优化:关于自动生成选择器

Zustand 是目前 React 生态里比较受欢迎的一个状态库,主要是因为用法上的简洁。

Zustand 简单使用

首先安装 zustand:

bash 复制代码
# NPM
npm install zustand
# Or, use any package manager of your choice.

接着从 zustand 库中引入 create API 就能创建同时包含状态和用于修改状态的方法的 Store 对象了。

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

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

这里,create() 接受一个回调函数用于定义初始 Store 中包含的内容:

  • bears 是状态
  • increasePopulation()、removeAllBears()、updateBears() 则是用于修改 bears 这个状态的方法,又叫 Action

同时,create() 返回的 useStore() 是一个 React Hook。useBearStore() 接收的是一个用于从 Store 中提取内容的回调函数,又叫"选择器(Selector)"。

接下来,就可以在你的组件中使用 useBearStore() 了。

你可以引入状态:

jsx 复制代码
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

也可以引入用于修改状态的方法:

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

如此一来,你在 <Controls /> 中调用修改 increasePopulation() 后,会触发 state.bears 的值加 1,接着就能在 <BearCounter /> 中看到新的值了。

掌握了以上关于 Zustand 的基本用法后,其实你就可以开发项目了。

不过,你想更进一步提升开发体验,那么就要去解决这个过程当中的一些使用痛点。

其中一个是关于更新嵌套状态的,这在之前的文章《React 状态库 Zustand 入门教程》 中有提到,有兴趣的读者可以移步阅读。

不过,我们本次关注的是另一个痛点:就是从 Store 中获取状态/Action的过程。

自动生成选择器

按照之前的介绍,在创建完 Store 之后,我们每次都要在组件中这样去使用:

jsx 复制代码
const bears = useBearStore((state) => state.bears)

我们要使用这种方式从 Store 中提取状态或是 Action。

不过每次频繁这样去写这样一个选择器函数是很乏味的,这个时候我们就可以考虑借助一个工具函数,对我们的 Store 进行增强,支持状态/Action的快捷访问。

这就是我们要介绍的 createSelectors(store) 函数了------先亮代码:

jsx 复制代码
const createSelectors = (store) => {
  store.use = {}
  for (let k of Object.keys(store.getState())) {
    store.use[k] = () => store((s) => s[k])
  }
  return store
}

代码量并不多,也好理解。

createSelectors() 接收的 store 就是前一节的 useBearStore,也就是 create() 的返回值。

createSelectors() 的作用很简单,就是向 store 中添加一个 .use 属性,用于快捷访问其上的内容。

当然,这里有一个隐藏的点,就是可以通过 store.getState() 拿到当前 Store 的所有内容。

接着,修改之前的内容------为了便于区分,我们将原来的 useBearStore 该名称 useBearStoreBase 了。经 createSelectors() 处理后,返回的是 useBearStore。

diff 复制代码
- const useBearStore = create((set) => ({
+ const useBearStoreBase = create((set) => ({
  // ...
}))

+ const useBearStore = createSelectors(useBearStoreBase)

现在,修改组件中使用 useBearStore 的地方。

diff 复制代码
function BearCounter() {
-  const bears = useBearStore((state) => state.bears)
+  const bears = useBearStore.use.bears()
  return <h1>{bears} around here...</h1>
}
function Controls() {
-  const increasePopulation = useBearStore((state) => state.increasePopulation)
+  const increasePopulation = useBearStore.use.increasePopulation()
  return <button onClick={increasePopulation}>one up</button>
}

减少了一些代码量,但是积少成多,也会提升一些开发体验。

不过需要注意的是,每次使用 .use 获取不管是状态还是 Acton 时,都要带上 () 的调用后缀。

jsx 复制代码
// 不管是状态还是 Action,后面都要带上 `()`
const bears = useBearStore.use.bears()
const increasePopulation = useBearStore.use.increasePopulation()

当然,现在项目中大都使用 TypeScript,为了获得更好的类型提示,我们对 createSelectors() 进行改造,添加类型注解。

ts 复制代码
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,
) => {
  let store = _store as WithSelectors<typeof _store>
  store.use = {}
  for (let k of Object.keys(store.getState())) {
    ;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
  }

  return store
}

这也是官方给出的方案,如果你对 TypeScript 不够熟悉也没关系,直接将上述代码贴到项目中使用即可。

总结

本文我们讲解了在使用 Zustand 时的一个小优化,关于自动生成选择器。借助 createSelectors(),我们可以更加轻松、快捷的访问 Store 中的状态或是 Action。

好了,希望本文的内容对你的工作有所帮助。感谢阅读,再见。

相关推荐
饼干哥哥8 分钟前
搭建一个云端Skills系统,随时随地记录TikTok爆款
前端·后端
酉鬼女又兒36 分钟前
零基础快速入门前端Web存储(sessionStorage & localStorage)知识点详解与蓝桥杯考点应用(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·html
DanCheOo41 分钟前
# 从"会用 AI"到"架构 AI":高级前端的认知升级
前端·ai编程
社恐的下水道蟑螂43 分钟前
前端面试必问 Git 通关指南:常用命令速查 + merge/rebase 深度辨析,看完再也不慌
前端·git·面试
angerdream1 小时前
最新版vue3+TypeScript开发入门到实战教程之组件通信之二
javascript·vue.js
小只笨笨狗~1 小时前
解决objectSpanMethod与expand共存时展开后表格错位问题
开发语言·javascript·ecmascript
None3211 小时前
NestJS 流式文件上传实践:从 Multer 到 Busboy 的进阶之路
前端·后端
海浪浪1 小时前
Symbol 产生的背景以及应用场景
前端·javascript
DROm RAPS1 小时前
十七:Spring Boot依赖 (2)-- spring-boot-starter-web 依赖详解
前端·spring boot·后端
OpenTiny社区1 小时前
GenUI SDK v1.1.0 正式发布|全端体验革新,能力与稳定性进阶
前端·ai编程