状态管理Zustand总结

432 阅读11分钟

发问:为什么需要状态管理库,用React context不香吗?

React遵循单向数据流的设计原则。当多个组件需要共享状态时,通常的做法都是把状态提升至离它们最近的父组件来进行维护。再通过Props逐层向下进行传递,这就是著名的Props drilling问题。

Prop drilling 是在React 应用中常见的问题,指的是为了将数据从一个顶层组件传递到一个深层嵌套的子组件,不得不经过多个中间组件层层传递props。 虽然这种方法是可行的,但它会导致代码变得冗长、难以维护,并且增加了中间组件的复杂性,因为它们需要传递与自身无关的props。

后来。React官方出手啦,React 16.3版本推出了 React context来创建全局上下文,使用context.provider组件包裹根组件来达到状态共享,需要获取状态的组件通过 useContext来进行组件的消费。

结合Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。

这样说来,感觉React context已经够用了!不过。使用context会遇到一些常见的问题,当provider中的context值发生改变时,会造成整棵子树都重新Rerender。这时你能想到的解决办法是:使用React.memo高阶组件订阅Props的变化、usememo钩子缓存计算昂贵的值,usecallback钩子缓存函数。或者将状态切分细一点,切成多个Provider进行提供。但这无疑增加了我们的心智负担,管理和优化的地方过多了!

状态管理库应运而生!现在有很多状态管理库,最有名的肯定是Redux,早期的状态管理库,并且有着生态丰富的优点,但是缺点是学习成本偏高,而zustand则是一个小而美的状态管理库,它使用高阶函数和hook来管理状态,可以无痛接轨React项目,并且还做了很多优化,减少了很多不必要的渲染,可以有选择的解构出useStore里的状态或方法,只有状态或方法发生改变时才会重新渲染。而不需要我们人为用一些优化手段(通常我们都会优化不完善)。并且还提供了一些中间件,在React应用中我们不希望修改状态对象,但如果每次都创建新对象又会带来额外的性能开销,它提供了immer以最小的成本实现了不可变数据结构。提供了persist进行本地化存储,提供了devtools来进行store的调试

Zustand 库使用了选择器 (selectors) 函数和引用相等性 (reference equality) 检查来帮助避免无效渲染。当你在组件中使用 Zustand 的 useStore 钩子时,你可以提供一个选择器函数来订阅特定的状态片段。Zustand 会使用严格相等性检查 (===) 来比较选择器返回的状态片段是否真的发生了变化,如果状态片段的值没有变化,组件不会重新渲染。

const bears = useStore((state) => state.bears);

React和Zustand采用Object.is来避免无效渲染。 Object.is 和 ===(严格相等运算符)在比较两个值时有一些区别,尤其是在处理特定类型的值时更为显著。

  1. NaN 和 NaN 的比较:
  • 使用 === 运算符时,NaN 不等于 NaN,即 NaN === NaN 的结果为 false。

  • 使用 Object.is 方法时,NaN 等于 NaN,即 Object.is(NaN, NaN) 的结果为 true。

  • +0 和 -0 的比较:

  • 使用 === 运算符时,+0 等于 -0,即 +0 === -0 的结果为 true。

  • 使用 Object.is 方法时,+0 不等于 -0,即 Object.is(+0, -0) 的结果为 false。

  • 其他值的比较:

  • 对于其他任何值,Object.is 的行为与 === 运算符完全相同,包括普通的数字、字符串、布尔值等。

1.什么是zustand

  • Zustand是一个德语单词,表示状态
  • Zustand是一个轻量级的JavaScript状态管理库,用于在React应用程序中管理状态
  • zustand使用高阶函数和hooks来管理状态,具有极高的灵活性和易用性,使开发人员可以快速、方便地开发React应用程序
  • zustand被称为当下复杂状态管理的最佳选择

2. zustand和其他状态管理库的比较

  • 简洁易用:zustand具有简洁的API,不需要过多的配置,易于使用。它不需要学习复杂的概念和语法,可以快速上手
  • 高效:zustand使用了高阶函数和hooks来管理状态,具有极高的效率和性能
  • 灵活:zustand的灵活性极高,可以满足不同的业务需求。开发人员可以根据自己的需求来定制自己的状态管理方案
  • 易于集成:zustand可以快速集成到现有的React项目中,不需要对现有代码进行大量的改动

3.它和Redux有什么区别?

Zustand 和 Redux 都是流行的状态管理库,用于管理 React 应用程序中的状态。它们之间的主要区别包括以下几点:

  1. API 和用法:
  • Redux: Redux 是一个基于 Flux 架构的状态管理库,它使用统一的 Store 来存储整个应用程序的状态。Redux 使用纯函数来处理状态的变化,通过 reducer 函数来处理 action,并通过中间件来扩展 Redux 的功能。Redux 还提供了一系列辅助函数和工具来简化状态管理的流程。
  • Zustand: Zustand 是一个基于 Hooks 的状态管理库,它与 React 更紧密地集成在一起。Zustand 允许您在组件中使用 useState 风格的 API 来创建和管理状态。与 Redux 不同,Zustand 不需要单独的 Store,每个状态都是一个独立的 Hook,因此可以更灵活地管理状态。
  • 复杂性:
  • Redux: Redux 通常被认为是一个相对复杂的库,特别是对于初学者来说,需要理解一些概念,比如 actions、reducers、middleware、selectors 等。虽然 Redux 提供了强大的工具和生态系统,但有时可能需要花费一些时间来学习和实现。
  • Zustand: 相比之下,Zustand 更加简单直观。它不需要编写独立的 reducers 和 actions,而是将状态逻辑直接放在组件中。这样可以减少一些繁琐的代码,并使状态管理更加直观。
  • 性能:
  • Redux: Redux 在性能方面表现良好,因为它使用了严格的不可变数据模式和单一的状态树,这使得状态的变化可以被有效地跟踪和管理。
  • Zustand: Zustand 也被设计成具有良好的性能。由于每个状态都是一个独立的 Hook,因此状态更新可以更加精确地触发,并且不会影响到其他状态。此外,Zustand 还提供了许多性能优化选项,例如订阅的选择和批量更新。

总的来说,Redux 适用于大型、复杂的应用程序,需要严格的状态管理和数据流程控制;而 Zustand 更适用于中小型的应用程序,可以更快地上手和使用,同时也提供了足够的灵活性和性能。选择哪个库取决于您的项目需求、团队的经验水平以及个人偏好。

最佳实践:

目前。我认为redux已经可以被zustand替代了。服务端状态用 React-query库进行管理更好,客户端状态用 zustand进行管理

特性ZustandRedux
状态模型不可变状态不可变状态
Context不需要需要使用 Provider
API简洁标准 Redux 需要 action、reducer;Redux Toolkit 提供简化的 API
代码样板较少较多(尽管 Redux Toolkit 有所简化)
渲染优化手动使用选择器手动使用选择器(Redux Toolkit 中 selector 的使用更为普遍)
状态更新直接通过 store 函数通过 dispatch 和 reducer
中间件支持有支持有支持,中间件生态丰富
其他无需包装应用,易于集成广泛的社区和生态系统支持,适合大型应用

4.Zustand拆分状态几种方式?

  1. Multi-Store:把不同的数据和方法,拆分为多个彼此独立的 store.

抽离 Action 函数 getState()方法拿到 store 的数据对象 setState()方法修改 store 的数据对象

import { create } from 'zustand'
// 导入需要的中间件
import { persist, devtools } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

const useFishStore = create<FishType>()(
 immer( // 3. 简化变更数据操作的 immer 中间件
   devtools( // 2. 调试 Store 数据的中间件
     persist( // 1. 持久化数据的中间件
       () => {
         return { // store 中的数据
           fishes: 0 // 小鱼干的数量
         }
       },
       {
         name: 'fish-store' // 数据的存储名称
       }
     )
   )
 )
)

// 小鱼干自增+1
export const incrementFishes = () => {
 useFishStore.setState((state) => {
   state.fishes += 1
 })
}

// 重置小鱼干的数量
export const resetFishes = () => {
 useFishStore.setState((state) => {
   state.fishes = 0
 })
}

export default useFishStore

Single-Store:把不同的数据和方法,拆分为多个 slice 切片,最终,把多个 slice 合并成全局唯一的 Store.

拆成多个Slice文件,每个slice用 StateCreator创建。只是把create换成StateCreator了,其他都一毛一样。

1.用StateCreator创建slice store

import { StateCreator } from 'zustand'
// 导入全局唯一的 Store
import useStore from '@/store'

const createFishSlice: StateCreator<FishSliceType> = () => {
 return {
   // 小鱼干的数量
   fishes: 0
 }
}

// 小鱼干自增+1
export const incrementFishes = () => useStore.setState((state) => ({ fishes: state.fishes + 1 }))
// 重置小鱼干的数量
export const resetFishes = () => useStore.setState({ fishes: 0 })

export default createFishSlice

2.用create组装slice成一个store

const useStore = create<BearSliceType & FishSliceType>()(
 persist( // 配置数据持久化的中间件
   (...a) => {
     return {
       ...createBearSlice(...a), //把set,get用变量a收集起来传递给slice
       ...createFishSlice(...a)
     }
   },
   { // 持久化的配置项
     name: 'store',
     storage: createJSONStorage(() => sessionStorage)
   }
 )
)

5.Zustand的中间件有哪些?

1.persist中间件:用于对其进行持久化存储。

默认情况下,数据会被持久化到 localStorage 中。如果想自定义存储的位置,可以借助于createJSONStorage来进行配置。例如,下面的代码演示了如何把数据持久化存储到 sessionStorage 中:

import { create } from 'zustand'
// 1. 导入需要的中间件
import { persist, createJSONStorage } from 'zustand/middleware'

const useBearStore = create<BearType>()(
  // 2. 对当前 Store 中的数据进行持久化存储
  persist(
    (set, get) => {
      // store 中的数据、方法
      return {
        // 小熊的数量
        bears: 0,
        // ...省略其它的方法
      }
    },
    // 3. 必须提供一个 persist 的配置对象
    {
      // name 用来指定存储后的数据名称
      name: 'bear-store',
      // storage 用来自定义存储的位置
      storage: createJSONStorage(() => sessionStorage),
      partialize: (state) => { // 形参中的 state 是 Store 中所有的数据
    // 对数据进行过滤、筛选等处理操作...
      return 要持久化的数据对象
  }
    }
  )
)

export default useBearStore

2.devtools中间件

import { create } from 'zustand'
// 1. 导入需要的中间件
import { persist, createJSONStorage, devtools } from 'zustand/middleware'

const useBearStore = create<BearType>()(
 // 4. 在 Redux DevTools 中调试当前 Store 中的数据
 devtools(
   // 2. 对当前 Store 中的数据进行持久化存储
   persist(
     (set, get) => {
       // store 中的数据、方法
       return {
         // 小熊的数量
         bears: 0
         // ...省略其它的方法
       }
     },
     // 3. 必须提供一个 persist 的配置对象
     {
       // name 用来指定存储后的数据名称
       name: 'bear-store',
       // storage 用来自定义存储的位置
       storage: createJSONStorage(() => sessionStorage)
     }
   )
 )
)

export default useBearStore

3.immer中间件:简化数据的变更操作

(set, get) => {
 //store 中的数据、方法
 return {
   // 小熊的数量
   bears: 0,
   // 让小熊的数量自增+1
   incrementBears: () =>
     set((state) => { // ★★★ 请注意这里的 {},改用 immer 语法后,必须使用 {} 把修改数据的代码包裹起来
       state.bears += 1
     }),
   // 重置 bears 的数量
   resetBears: () =>
     set((state) => { // ★★★ 请注意这里的 {},改用 immer 语法后,必须使用 {} 把修改数据的代码包裹起来
       state.bears = 0
     }),
   // 根据 step 的值让 bears 数量自减
   decrementBearsByStep: (step = 1) =>
     set((state) => { // ★★★ 请注意这里的 {},改用 immer 语法后,必须使用 {} 把修改数据的代码包裹起来
       state.bears -= step
     }),
   // 延迟1秒后,让 bears 数量+1
   asyncIncrementBears: () => {
     setTimeout(() => {
       // 注意这里 get() 方法的调用,它可以获取到 store 对象,并访问 store 中的数据或方法
       get().incrementBears()
     }, 1000)
   }
 }
}

4.subscribeWithSelector中间件

zustand不是使用Object.is()判断来避免Rerender嘛,为啥还要订阅某些状态的变化。 这是因为我们可以在某些状态发生改变时,做一些处理,比如 bear的数量达到5时让背景变成绿色。

基于subscribeWithSelector这个中间件,可以订阅(监听) Store 中指定数据的变化。它的使用分为以下两个主要步骤:

1.导入subscribeWithSelector中间件,并在创建 Store 的 Hook 是使用此中间件:

// 按需导入中间件
import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create(
 // 使用中间件
 subscribeWithSelector(() => ({ name: 'zs', age: 20 }))
)

2.调用subscribe()函数订阅具体数据的变化:

//const unsubFn = useStore.subscribe(selectorFn, cb, options?)

const unsubFn = useStore.subscribe(state => state.age, (newAge, oldAge) => {
 console.log(newAge, oldAge)
}, { fireImmediately: true })
//其中 options 配置对象中的 fireImmediately: true 表示立即触发一次回调函数的执行。

当然在 zustand 中,subscribe(fn)可以用来订阅 Store 数据的变化,并在数据变化后执行 fn 回调函数。 在回调函数中,接收两个形参 newValue 和 oldValue,其中: newValue:表示变化后的新值 oldValue:表示变化前的旧值 同时,subscrible() 还返回一个取消订阅的函数。语法格式如下: const unsubFn = useStore.subscribe((newValue, oldValue) => { console.log(newValue, oldValue) }) subscribe 的缺点:只能订阅整个 Store 数据的变化,无法订阅 Store 下某个具体数据的变化。

官方建议的中间件的调用顺序

此外,我们建议尽可能最后使用 devtools 中间件。例如,当您将它与 immer 一起用作中间件时,它应该是immer(devtools(…))而不是devtools(immer(…))。

从外到内

subscribeWithSelector 、immer、devtools、persist