发问:为什么需要状态管理库,用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 和 ===(严格相等运算符)在比较两个值时有一些区别,尤其是在处理特定类型的值时更为显著。
- 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 应用程序中的状态。它们之间的主要区别包括以下几点:
- 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进行管理
| 特性 | Zustand | Redux |
|---|
| 状态模型 | 不可变状态 | 不可变状态 |
|---|
| Context | 不需要 | 需要使用 Provider |
|---|
| API | 简洁 | 标准 Redux 需要 action、reducer;Redux Toolkit 提供简化的 API |
|---|
| 代码样板 | 较少 | 较多(尽管 Redux Toolkit 有所简化) |
|---|
| 渲染优化 | 手动使用选择器 | 手动使用选择器(Redux Toolkit 中 selector 的使用更为普遍) |
|---|
| 状态更新 | 直接通过 store 函数 | 通过 dispatch 和 reducer |
|---|
| 中间件支持 | 有支持 | 有支持,中间件生态丰富 |
|---|
| 其他 | 无需包装应用,易于集成 | 广泛的社区和生态系统支持,适合大型应用 |
|---|
4.Zustand拆分状态几种方式?
- 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