2.基础教程-应用结构
创建 Redux Store
Redux Slice
“slice” 是应用中单个功能的 Redux reducer 逻辑和 action 的集合, 通常一起定义在一个文件中。
比如,在一个博客应用中,store 的配置大致长这样:
1 | import { configureStore } from "@reduxjs/toolkit" |
创建 Slice Reducer 和 Action
1 | import { createSlice } from "@reduxjs/toolkit" |
Redux Toolkit 有一个名为 createSlice
的函数,它负责生成 action 类型字符串、action creator 函数和 action 对象。
createSlice
内部使用了一个名为 Immer 的库。 Immer 使用一种 “Proxy” 包装你提供的数据,当你尝试 ”mutate“ 这些数据的时候,Immer 会跟踪你尝试进行的所有更改,然后使用该更改列表返回一个安全的、不可变的更新值,就好像你手动编写了所有不可变的更新逻辑一样。
Reducer 的规则
- 仅使用
state
和action
参数计算新的状态值 - 禁止直接修改
state
。必须通过复制现有的state
并对复制的值进行更改的方式来做 _不可变更新(immutable updates)_。 - 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码
“不可变更新(Immutable Updates)” 这个规则尤其重要,值得进一步讨论。
Reducer 与 Immutable 更新
在 Redux 中,**永远 不允许在 reducer 中直接更改 state 的原始对象!**
1 | // ❌ 非法 - 默认情况下,这将更改 state! |
这就是为什么 Redux Toolkit 的 createSlice 函数可以让你以更简单的方式编写不可变更新!
警告
你 只能 在 Redux Toolkit 的
createSlice
和createReducer
中编写 “mutation” 逻辑,因为它们在内部使用 Immer!如果你在没有 Immer 的 reducer 中编写 mutation 逻辑,它 将 改变状态并导致错误!
用 Thunk 编写异步逻辑
到目前为止,我们应用程序中的所有逻辑都是同步的:
- dispatch action
- store 调用 reducer 来计算新状态
- dispatch 函数完成并结束
但是,我们的应用程序通常具有异步逻辑,我们需要一个地方在我们的 Redux 应用程序中放置异步逻辑。
thunk 是一种特定类型的 Redux 函数,可以包含异步逻辑。Thunk 是使用两个函数编写的:
- 一个内部 thunk 函数,它以
dispatch
和getState
作为参数 - 外部创建者函数,它创建并返回 thunk 函数
示例:
1 | // 外部的 thunk creator 函数, 它使我们可以执行异步逻辑 |
显然,incrementAsync()
返回的不是 action(action 是具有type
字段的纯函数),而是一个函数,但它的使用方式和普通的 action 是一样的:
1 | export function Counter() { |
这是依赖 “middleware” 机制实现的,Redux 的 store 可以使用 “middleware” 进行扩展,中间件是一种可以添加额外功能的附加组件或插件。其最常见的用途就是实现异步逻辑,同时仍能与 store 对话。
Redux Thunk 中间件,代码很短:
1 | const thunkMiddleware = |
它先判断传入 dispatch
的 action 是函数还是对象。如果是一个函数,则调用函数,并返回结果。否则,传入的是普通 action 对象,就把这个 action 传递给 store 处理。
React Counter 组件
1 | import React, { useState } from "react" |
使用 useSelector 提取数据
useSelector
这个 hook 让我们的组件从 Redux 的 store 状态树中提取它需要的任何数据。
我们默认 组件中不能引入 store。所以useSelector
负责在幕后与 Redux store 对话。
示例:
1 | const countPlusTwo = useSelector(state => state.counter.value + 2) |
useSelector
会调用 store.getState()
获取 state,然后返回 state.counter.value + 2
的值。
每当一个 action 被 dispatch 并且 Redux store 被更新时,useSelector
将重新运行我们的选择器函数。如果选择器返回的值与上次不同,useSelector
将确保我们的组件使用新值重新渲染。
使用 useDispatch 来 dispatch action
类似地,我们知道如果我们可以访问 Redux store,可以 store.dispatch(increment())
。
由于我们无法访问 store 本身,因此我们需要某种方式来访问 dispatch
方法。
useDispatch
hook 为我们完成了这项工作,并从 Redux store 中为我们提供了实际的 dispatch
方法:
1 | const dispatch = useDispatch() |
组件 State 与表单
在 React + Redux 应用中,你的全局状态应该放在 Redux store 中,你的本地状态应该保留在 React 组件中。
大多数表单的 state 不应该保存在 Redux 中。 相反,在编辑表单的时候把数据存到表单组件中,当用户提交表单的时候再 dispatch action 来更新 store。
Providing the Store
我们已经看到我们的组件可以使用 useSelector
和 useDispatch
这两个 hook 与 Redux 的 store 通信。奇怪的是,我们并没有导入 store,那么这些 hooks 怎么知道要与哪个 Redux store 对话呢?
答案是使用 Context
:
1 | import React from "react" |
总结
我们可以使用 Redux Toolkit configureStore API 创建一个 Redux store
configureStore
接收reducer
函数来作为命名参数configureStore
自动使用默认值来配置 store
在 slice 文件中编写 Redux 逻辑
- 一个 slice 包含一个特定功能或部分的 state 相关的 reducer 逻辑和 action
- Redux Toolkit 的
createSlice
API 为你提供的每个 reducer 函数生成 action creator 和 action 类型
Redux reducer 必须遵循以下原则
- 必须依赖
state
和action
参数去计算出一个新 state - 如果要修改 state,只能先拷贝 state 副本,然后去修改副本
- 不能包含任何异步逻辑或其他副作用
- Redux Toolkit 的
createSlice
API 内部使用了 Immer 库才达到表面上直接修改(”mutating”)state 也实现不可变更新(_immutable updates_)的效果
- 必须依赖
一般使用 “thunks” 来开发特定的异步逻辑
- Thunks 接收
dispatch
和getState
作为参数 - Redux Toolkit 内置并默认启用了
redux-thunk
中间件
- Thunks 接收
使用 React-Redux 来做 React 组件和 Redux store 的通信
- 在应用程序根组件包裹
<Provider store={store}>
使得所有组件都能访问到 store - 全局状态应该维护在 Redux store 内,局部状态应该维护在局部 React 组件内
- 在应用程序根组件包裹