技术分享
🗒️React18 核心与实战(二)状态管理工具
00 分钟
2024-11-5
2024-11-5
type
status
date
slug
summary
tags
category
icon
password

常见的状态管理工具

常见的 React 状态管理工具种类繁多,以下是一些主流工具的简介、特点以及适合的场景:

1. React Context + useReducer

  • 简介:React Context 和 useReducer 是 React 内置的工具,用于创建全局状态管理方案。通过 Context 提供状态,通过 useReducer 管理状态逻辑。
  • 特点
    • 轻量化:内置工具,无需额外安装包。
    • 简洁:适合处理小型全局状态,不依赖复杂的 API。
  • 适用场景:适合简单的全局状态,比如用户认证、主题管理等小规模状态需求。
  • 缺点:对于大型应用,嵌套过深的组件可能会导致复杂度增加,并且缺乏中间件支持,扩展性有限。

2. Redux (推荐使用 Redux Toolkit)

  • 简介:Redux 是一个老牌的 JavaScript 状态管理库,Redux Toolkit 是它的简化版本,提供了更直观和简洁的用法。
  • 特点
    • 强大而规范:提供集中式的状态存储,能更清晰地跟踪状态流动。
    • Redux Toolkit:封装了一些 Redux 的复杂性,集成了异步操作、reducers 和状态管理工具。
    • DevTools 支持:提供非常强大的调试工具和中间件扩展。
  • 适用场景:适合复杂的、跨组件和模块间状态管理需求;推荐在大型应用中使用。
  • 缺点:Redux 本身的学习曲线较高,不过 Redux Toolkit 简化了部分工作流程。

3. MobX

  • 简介:MobX 是基于响应式编程的状态管理工具,它使用可观察的状态(observable state),当状态改变时,UI 自动更新。
  • 特点
    • 响应式:状态变化时,依赖状态的组件会自动重新渲染。
    • 代码简洁:非常适合数据流简单的应用,使用少量代码即可实现复杂的逻辑。
  • 适用场景:适合对响应式数据要求较高的应用;尤其在表单状态管理、管理实时数据方面较为优越。
  • 缺点:由于响应式特性较强,复杂的状态管理可能导致难以调试的场景,代码组织不当会产生副作用。

4. Recoil

  • 简介:Recoil 是 Facebook 开发的状态管理工具,设计与 React 深度集成,特别适合状态较复杂的项目。
  • 特点
    • 简单的依赖管理:通过 atoms 和 selectors 管理和计算派生状态。
    • 高效的状态共享:多个组件可以轻松共享同一个状态,同时保持状态独立更新。
  • 适用场景:适合状态依赖关系复杂的项目,尤其是复杂的数据关系计算和动态更新的场景。
  • 缺点:社区生态和工具链还不够成熟,尤其是与大型项目配合时,稳定性和工具支持略显不足。

5. Zustand

  • 简介:Zustand 是一个轻量的状态管理库,它的 API 简单、体积小且灵活,适合管理局部和中小型项目的状态。
  • 特点
    • 轻量且快速:不依赖上下文,轻量化的状态管理且高效。
    • 支持 React Server Components:可以配合使用 React Server Components 以支持 SSR。
  • 适用场景:适合小型或局部状态管理;对性能要求高但状态简单的项目,如微型应用和局部状态管理。
  • 缺点:功能有限,对于大型项目可能需要额外的状态管理和扩展支持。

6. XState

  • 简介:XState 是状态机和状态图工具,它将状态视为一个状态机,非常适合控制复杂流程和多步骤操作。
  • 特点
    • 状态机模型:适合于严格的状态管理和流程控制。
    • 灵活的状态流:支持异步状态和复杂的状态切换,非常适合多步骤表单、复杂交互。
  • 适用场景:非常适合多步骤流程、复杂的用户交互控制和工作流应用。
  • 缺点:学习曲线相对较高,对项目的设计要求高;一般用于特定的复杂流程,而不是全局状态管理。

状态管理工具的选择总结

  • 小型应用:可以使用 React Context + useReducerZustand
  • 中型应用:使用 ZustandRecoil 管理状态。
  • 大型应用:推荐使用 Redux Toolkit,大型项目中需要清晰的状态流。
  • 复杂工作流和用户交互XState 是最适合的,尤其是需要状态机模型的场景。
这些工具可以根据应用的具体需求进行组合使用,比如在全局状态使用 Redux,局部状态或复杂流程使用 XState。

Redux

什么是Redux

Redux是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行。
作用:通过集中管理的方式管理应用的状态。
Redux 的核心理念是:单一状态树不可变状态纯函数更新
notion image

快速初体验

需求: 不和任何框架绑定,不适用任何构建工具,使用纯Redux实现计数器。
notion image
使用步骤:
  1. 定义一个reducer函数(根据当前想要做的修改返回一个新的状态)
  1. 使用createStore方法传入reducer函数,生成一个store实例对象
  1. 使用store实例的subscribe方法订阅数据的变化(数据一旦变化,可以得到通知)
  1. 使用store实例的dispatch方法提交action对象触发数据变化(告诉reducer你想怎么改数据)
  1. 使用store实例的getState方法获取最新的状态数据更新到视图中。

Redux 的核心由以下几个概念组成:

  • Store:整个应用的状态以一个对象的形式存储在 Store 中,Store 是应用的唯一数据源。一个对象,存放着我们管理的数据状态
  • Action:表示希望进行的操作或状态更改,通常包含 typepayload(数据)两个字段。一个对象,用来描述你想怎么改数据
  • Reducer:一个纯函数,接受当前的状态和 action,根据 action 的类型返回新的状态。一个函数,根据action的描述生成一个新的state
  • Dispatch:用于将 action 发送到 reducer,由 reducer 更新状态。
  • Selector:用于从 Store 中提取所需的状态数据。

Redux 的工作流程

  1. Dispatch Action:组件或业务逻辑中调用 dispatch(action),触发状态更新流程。
  1. Reducer 处理:Reducer 接收当前状态和 action,使用纯函数返回新的状态对象。
  1. Store 更新:Redux Store 通过 Reducer 的返回值更新全局状态。
  1. UI 更新:当 Store 更新后,UI 自动重新渲染,反映新的状态。
例如,我们可以设计一个简单的计数器应用,用户点击按钮时,计数值增加或减少。
notion image

Redux 基本用法

Redux 可以通过 Redux Toolkit 实现简化的开发体验.在React中使用Redux,官方要去安装两个其他插件:Redux Toolkit 和 react-redux。
Reux Toolkit(RTK):官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
notion image
react-redux:用了链接Redux和React组件的中间件。
notion image

1. 安装 Redux 和 Redux Toolkit

store目录结构设计
notion image
  1. 通常集中状态管理的部分都会单独创建一个单独的store目录。
  1. 应用通常会有很多个子store模块,所以创建一个modules目录,在内部编写业务分类的子store。
  1. store中的入口文件index.js的作用是组合modules中所有的子模块,并导出store。
notion image

2. 定义一个 Slice(modules中的xxxStore.js)

Redux Toolkit 使用 “slice” 来管理状态,每个 slice 包含状态和相关的 reducer。
reducer与action
createSlice 中,定义的函数实际上是 reducer,而不是直接的 action。不过,这些 reducer 函数会自动生成对应的 action creators。让我们详细解释一下这两者之间的关系。
1. Reducer 与 Action 的区别
  • Reducer:是一个纯函数,用于描述如何根据 action 来更新应用的状态。它接受两个参数:当前的状态和一个 action,并返回新的状态。Reducer 不应有副作用,也不应修改传入的状态对象。
    • 例如:
  • Action:是一个普通的 JavaScript 对象,描述发生了什么。每个 action 至少有一个 type 字段来标识动作。可以选择性地包含其他信息(payload)。
    • 例如,incrementdecrement 这两个 action 的结构可能如下:

2. 从 Reducer 自动生成 Action Creators

当你使用 createSlice 定义 reducers 时,Redux Toolkit 会自动为每个 reducer 生成一个对应的 action creator。这个 action creator 是一个函数,当被调用时,会返回一个 action 对象。
例如,以上面的 counterSlice 为例:
  • increment reducer 自动生成的 action creator 可以通过 counterSlice.actions.increment() 来调用,它会返回 { type: "counter/increment" } 的 action。
  • 同理,decrement reducer 也会生成一个对应的 action creator。

3. 配置 Store(出口的index.js)

将定义好的 slice reducer 加入 Redux Store。
当使用 export default 导出一个值时,在导入的地方可以使用任意的变量名来接收这个被导出的值。这是因为 export default 提供了一种默认的导出方式,导入时并不需要与导出时的名称完全对应。counterSlice.reducer被默认导出,所以在导入时可以使用任何合法的变量名来接收这个 reducer,比如可以将 import counterReducer from './counterSlice'; 中的 counterReducer 改为其他变量名,只要在后续使用这个导入的 reducer 的地方使用新的变量名即可。
补充学习命名导出和默认导出
1. 命名导出 vs 默认导出
  • 命名导出:可以导出多个变量,每个变量都需要用 export 语句单独导出。导入时需要使用相同的名称。
    • 示例:
      导入时必须保持一致:
      如果尝试使用不同的名称,编译器会报错:
  • 默认导出:每个模块只能有一个默认导出,导入时可以使用任何名称。
    • 示例:
      导入时可以自定义名称:
2. 在 Redux 中的使用
在你之前的例子中,incrementdecrement 是通过命名导出从 counterSlice 导出的:
在组件中使用时,你必须保持这些名称的一致性:
如果你尝试使用其他名称:
3. 导入所有导出
如果你希望导入一个模块中所有的命名导出,可以使用 * 语法:
在这种情况下,counterActions 作为命名空间,将所有的导出包含在一个对象中,便于管理和使用。
总结
  • 如果使用命名导出,在导入时必须保持字段名称一致。
  • 如果使用默认导出,导入时可以自由选择名称。
  • 使用命名空间导入可以将所有命名导出放在一个对象中,方便使用和管理。
关于configureStore
configureStore 是 Redux Toolkit 中提供的一个函数,用于创建 Redux store。它简化了 Redux store 的配置过程,提供了一些默认配置,以减少开发者的工作量。以下是 configureStore 的主要作用和一些关键特性。

1. 创建 Store

configureStore 的基本作用是创建一个 Redux store。它接收一个配置对象,包含 reducers 和其他中间件等选项。

2. 自动配置中间件

configureStore 会自动为你配置 Redux Thunk 中间件,这是处理异步操作(如 API 请求)的常用中间件。你也可以添加其他自定义中间件。

3. 开发工具支持

使用 configureStore 创建的 store 默认集成了 Redux DevTools,这使得调试和监控 Redux 状态变得更容易。你可以在浏览器中使用 Redux DevTools 插件查看状态变化和 dispatched actions。

4. 配置增强

configureStore 还允许你轻松地添加其他增强(enhancers),例如支持 Redux Persist 等功能。

5. 易于使用

与传统的 Redux store 创建方法相比,configureStore 简化了流程。你不需要手动设置中间件、增强和默认的 Redux DevTools 支持,减少了样板代码。

示例

下面是一个完整的示例,展示了如何使用 configureStore 创建 Redux store:

总结

configureStore 的主要作用是简化 Redux store 的创建和配置过程,它自动处理中间件、开发工具支持和其他配置选项,使得 Redux 的使用更加直观和方便

4. 在根组件中提供 Store

react-redux负责把Redux和React链接起来,内置Provider组件,通过store参数把创建好的store实例注入到应用中,链接正式建立。
index.js 文件中使用 Provider 包裹应用,并传入 Store:

5. 在 React 组件中使用 Redux

在React组件中使用store中的数据,需要用到一个钩子函数 useSelector,它的作用是把store中的数据映射到组件中.
组件中可以使用 useSelector 获取状态, React组件中修改store中的数据需要借助另外一个hook函数: useDispatch,它的作用是生成提交action对象的dispatch函数,使用样例如下,用 useDispatch 发送 action。
notion image
notion image
provider和useSelector的区别

1. Provider 的作用

Provider 是 React-Redux 提供的一个组件,主要功能是将 Redux store 作为上下文(context)注入到 React 组件树中。这样,React 组件树中的所有子组件都可以通过 context 访问这个 store
在这个例子中,Providerstore 传递给整个应用的组件树。

2. React Context 的作用

Provider 使用 React 的 Context APIstore 传递给所有子组件。React 的 Context API 允许数据在组件树中向下传递,而不必通过手动的 props 传递。通过 Context,store 可以在整个组件树中共享。
Provider 内部,store 被作为 context 的值传递,因此任何嵌套在 Provider 内的组件都可以访问这个 context,而无需逐层传递 props

3. useSelector 的作用

useSelector 是 React-Redux 提供的一个 hook,用于从 Redux store 中选择特定的数据。它会订阅 store,并在所选择的状态更新时,触发组件重新渲染。useSelector 使用的 context 实际上是由 Provider 提供的。
当你在组件中使用 useSelector 时,它会:
  • 获取当前的 store(这个 store 是通过 Provider 传递的 context 提供的)。
  • 订阅 store 中的变化,以便在特定状态更新时重新渲染组件。
示例:
在这个例子中,useSelector 能够访问 Redux store 并选择 counter.value 的值。这是因为 Providerstore 提供为 context,useSelector 可以从中读取 store 并获取当前的状态。

4. 关联关系的建立

总结来说,ProvideruseSelector 的关联关系是通过以下步骤实现的:
  • Provider 使用 Context API 将 store 作为 context 传递给整个组件树。
  • useSelector 使用 context 来访问 store,并订阅 store 中的特定状态。
  • store 中的状态发生变化时,订阅的组件(使用 useSelector 的组件)会重新渲染,从而实现状态同步更新。

5. Provider 和 useSelector 之间的获取规则

React-Redux 的 Provider 会自动识别传递的 store 对象并将它传入 context 中,而 useSelector 会自动从这个特定的 context 中获取 store,与变量名无关。
例如,以下代码都可以正常工作:
在上面代码中,即使 myStore 不是命名为 storeProvider 仍然会知道这个对象是 Redux store 并传递给 context。useSelectorconnect 都会直接使用这个 context 中的 store,而不需要知道具体的变量名称。

原理:React-Redux 的 Provider 和 Context

  • Provider 是 React-Redux 特殊实现的一个组件,内部通过 Context API 提供 Redux store
  • useSelector 通过 Provider 的 context 获取 store,并从中读取 Redux 状态。
所以,当你在 Provider 中传递 Redux store 时,Provider 的代码会自动识别该对象为 store 并传入 context。组件中的 useSelector 会自动订阅这个 context,无需了解传递的变量名。
Provider 接收到一个对象作为 store,它会检查这个对象是否实现了上述方法(即符合 Redux store 的接口),从而确认该对象是 Redux store 实例。这种判断方式通常称为“鸭子类型”(duck typing),即通过检查对象的行为(而非其名称或类型)来判断它是什么。

代码示例

当我们传入任何符合 Redux store 规范的对象,不管变量名称如何,Provider 都会将其识别为 store 并传递到 context 中,例如:
Provider 识别出 customStore 是 Redux store 后,会通过 context 提供给组件树。
提交action传参
notion image
组件中有两个按钮:add to 10 和 add to 20 可以直接把count的值修改到对应的数字,目标count值是在组件中传递过去的,需要在提交action的时候传递参数。
实现步骤:在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传参,参数会传递到action对象payload属性上。 (要实现按钮点击后直接将 count 修改为 10 或 20,可以通过 Redux 的 action 来实现,使用 dispatch 方法时传递目标值作为参数)。以下是实现步骤:
  1. 定义 action:在 Redux slice 中定义一个 reducer,它能够根据传递的参数来更新 count 的值。
  1. 传递参数到 reducer:在 dispatch action 时,传递目标值作为参数,以更新到指定值(如 10 或 20)。

实现步骤

1. 在 Slice 中定义 Reducer

我们在 counterSlice.js 中的 reducers 定义一个新的 reducer,例如 setCount,它接受传入的参数并更新 count
这里 setCount 函数的 action.payload 就是传递的目标值,比如 10 或 20。

2. 配置 Store

确保将该 reducer 加入到 Redux store 中。

3. 在组件中使用 dispatchsetCount

在组件中,使用 useDispatchuseSelector 来触发 setCount 并读取 count 的值。

工作流程

  • 当点击 “Add to 10” 按钮时,handleSetCount 被调用并传入 10,然后 setCount(10)dispatch,从而将 count 更新为 10
  • 同理,当点击 “Add to 20” 按钮时,传入 20 并将 count 更新为 20

总结

通过在 reducer 中定义一个接收参数的 setCount 函数,并在 dispatch 时传递目标值,实现了直接将 count 设置到指定的值(如 10 或 20)的功能。这种方式让按钮可以轻松传递不同的目标值到 Redux store。
补充:多参数传递
假设我们需要在更新 count 的同时,还记录一个 message,可以将这两个数据作为对象传递。
  1. 定义 Action:在 reducer 中,使用 action.payload 解构对象中的多个属性。
  1. 组件中使用 dispatch 并传递多个参数
在组件中,可以将 countmessage 包装成一个对象,然后作为 payload 传递。

总结

  • 单参数:直接传递参数即可,action.payload 即是传入的值。直接使用值,比如数字、字符串等。
  • 多参数:通过对象传递多个参数,然后在 reducer 中通过解构从 action.payload 获取。通过对象封装,实际上还是一个单参数传递给 action.payload
 
异步状态操作
常规做法
  1. 创建store的写法保持不变,配置好同步修改状态的方法。
  1. 单独封装一个函数,在函数内部return一个新函数,在新函数中:
    1. 封装异步请求获取数据
    2. 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
  1. 组件中dispatch的写法保持不变。
代码解析
代码主要是使用 createSlice 定义了 channelStore,并用 fetchChannelList 封装了一个异步的 action creator,实现了以下逻辑:
  1. 创建 SlicechannelStore 使用 createSlice 定义,包含了 initialState 和一个用于更新状态的同步 reducer setChannelList
  1. 定义异步 ActionfetchChannelList 是一个异步函数,用于从 API 获取数据,然后通过 dispatch(setChannelList()) 将获取的数据传入同步 action,最终更新 Redux 的状态。
  1. 组件中使用异步 Action:在 App 组件中,useEffect 调用 dispatch(fetchChannelList()),从而触发异步请求。数据到达后 channelList 会更新并重新渲染。

为什么使用 [dispatch]

在函数组件中,每次组件重新渲染时都会创建新的函数闭包。为了让 useEffect 保证始终引用到最新的 dispatch,官方建议把它作为依赖项。
实际情况中,dispatch 的变化几乎不会发生,因此在大部分项目里,添加或不添加 [dispatch] 都不会有明显的差别。但是:
  • 建议规范:React 官方在某些代码检查工具(如 ESLint)中建议将 dispatch 作为依赖项,主要是为了保持一致性和减少潜在错误。
  • 稳定性:如果未来的某些情况下 dispatch 引用发生了变化,依赖项 [dispatch] 会确保 useEffect 能正确捕获。
这个代码的效果与 [] 作为依赖项类似,因为 dispatch 本身是 Redux 返回的稳定引用。
补充解构的用法
 
执行和渲染顺序
在 React 中,组件的渲染、useEffect 触发、异步请求的执行等有一个特定的顺序。接下来一步步地说明组件初始化、渲染和请求的逻辑顺序。
假设以下是一个典型的组件代码片段:

执行顺序和逻辑详解

  1. 组件初始化阶段
      • 代码被首次加载和解析。
      • useState 初始值(如 localState0)被设置。
      • useEffect 被注册,但并不立即执行(它会在渲染后执行)。
      这一步只准备初始状态和效果(effect),还没有与 DOM 交互。
  1. 首次渲染阶段
      • React 根据当前的状态和 JSX 来“绘制”组件的初始 UI。
      • return 中的 buttonul 标签被渲染,channelList 还为空(因为数据还没有请求到)。
      • 组件完成首次渲染并将初始 UI 显示在页面上。
  1. 执行 useEffect 中的异步请求
      • 在首次渲染结束后,useEffect 的内容开始执行。
      • dispatch(fetchChannelList()) 被调用,触发了一个异步请求。
      • 组件等待请求返回结果,但不会阻塞渲染流程(因为这是异步的)。
  1. 异步请求返回数据,更新 Redux 状态
      • 当请求完成后(假设成功获取数据),异步请求的结果通过 dispatch(setChannelList(res.data)) 更新 Redux 状态。
      • Redux 状态更新触发 Redux 与 React 的联动,组件监听到 channelList 发生了变化。
  1. 重新渲染组件
      • 由于 channelList 的变化,React 会重新渲染该组件以反映最新的状态。
      • 在这次渲染中,ul 中的列表项会显示获取到的 channelList 数据。
  1. 用户交互(点击按钮)
      • 当用户点击“Increase”按钮时,触发 handleClick,这会调用 setLocalState(localState + 1) 更新 localState
      • setLocalState 的调用会使组件的 localState 发生变化,导致组件重新渲染。
      • 在新的渲染中,button 显示 localState 的新值(增加了 1)。

渲染与 useEffect 的交替

React 的组件会按照上述逻辑循环执行渲染与 useEffect
  • 渲染 → useEffect → 异步更新状态 → 再次渲染
  • 每次 stateprops 发生变化,React 都会重新渲染,渲染后会再触发对应的 useEffect(如果依赖项发生变化)。

关键点总结

  • useEffect 总是在渲染后执行,不会阻塞渲染。
    • useEffect依赖项是[变量名]的处理逻辑
      在使用 useEffect 时,依赖项数组(如 [变量名])的存在确实会影响其执行时机。具体来说,依赖项的执行时机可以总结为以下两个主要情况:
      1. 组件首次渲染后执行
      • 当组件首次挂载到 DOM 中时,useEffect 中的函数会在浏览器完成渲染后立即执行。这是为了确保组件已经被完全渲染到页面上,副作用(如数据请求、事件监听等)能够正常工作。
        • 例子:如果您在 useEffect 中发起网络请求,它将在组件的首次渲染后执行,允许您在 UI 渲染后获取数据。
      2. 依赖项变化时执行
      • 当依赖项数组中的某个变量发生变化时,React 会重新执行 useEffect 中的函数。这意味着如果您将某个状态或属性作为依赖项,当这个状态或属性的值变化时,useEffect 会被重新调用。
        • 例子:假设您有一个状态变量 count,并且您将其放入依赖项数组中:[count]。那么每次 count 的值变化时,useEffect 中的函数都会被重新执行。
      注意事项
      • 清理函数:如果您在 useEffect 中返回一个清理函数(cleanup function),该函数会在下次执行 useEffect 之前或者组件卸载时被调用。这使得您可以清理任何在 useEffect 中创建的副作用(如取消请求、移除事件监听等)。
      • 无依赖项数组:如果不传入依赖项数组,useEffect 将在每次渲染后都执行。
      例子总结
      以下是一个简单的例子来说明这两种执行时机:
      • Counter 组件首次渲染时,useEffect 中的函数会执行,输出“Component mounted or count changed: 0”。
      • 每当您点击按钮并增加 count 时,useEffect 中的函数会再次执行,输出“Component mounted or count changed: [新值]”。
      • 如果在 useEffect 中定义了清理函数,它会在下次执行 useEffect 前或组件卸载时被调用
  • 异步请求的状态更新会触发组件重新渲染。
  • React 会确保 useEffect 在渲染完成后再执行,所以即使请求未完成,用户仍然能看到组件的初始渲染。

Redux 的最佳实践

  • 使用 Redux Toolkit:它简化了 Redux 的开发,避免了繁琐的样板代码。
  • 将业务逻辑放入 Reducer 中:避免在组件中处理业务逻辑,保持组件的简洁。
  • 选择合适的状态:只将全局需要的状态放入 Redux,局部状态使用组件内部状态管理。
  • 分层设计:将 Redux 状态和 UI 组件解耦,尽量减少 UI 直接依赖全局状态。
  • 避免嵌套过深的状态:保持状态结构简单,以便于维护和更新。

Redux 的常见场景

  • 全局的用户认证信息:如用户登录状态、用户信息等。
  • 共享的应用设置:如主题颜色、语言等设置。
  • 跨组件的复杂数据流:如电商购物车、订单管理等。

注意事项

  • 异步逻辑:Redux 本身不处理异步逻辑,Redux Toolkit 提供了 createAsyncThunk 来处理异步操作。
  • 性能优化:Redux 是集中式的状态管理系统,频繁的大量状态更新可能会影响性能,适时使用分层管理或者 React Context 来优化性能。

Redux调试-devtools

Redux 官方提供了针对Redux的调试工具,支持实时state信息展示,action提交信息查看等。