最近在研究 Redux。看了中文文档感觉翻不太行,所以我在读英文文档,但由于我英文水平不太行,本文将会以中文为主展现,主要作为个人记忆用。
感觉 Redux 这个入门文档有点杂,掺入了官方推荐工具Redux-Toolkit
和Redux-React
的内容。本篇介绍的 Redux 是借用这些工具使用的。
基本配置
创建store
Redux 的 store 由 RTK(Redux Toolkit)的 configureStore
api 创建。
文档建议此段代码位置在 /src/app/store.js
中
1 | import { configureStore } from "@reduxjs/toolkit"; |
从文档中的代码可以看出configureStore
用法:
- 输入 一个对象
- reducer(object)
- 切片名:(createSlice()创建的切片.reducer)
- reducer(object)
- 输出 一个 store 对象
其中 store 对象会被export default
,并且 store 对象只能用于Provider
中,即index.js
中的<Provider store={store}>
一个 React App 建议(或者说 应当)只有一个 store
用Provider
覆盖 React 根结点
将 React 原本的<App />
修改为:
1 | <Provider store={store}> |
在 React App 的入口文件index.js
中使用由Redux-React
提供的Provider
将根节点覆盖住,并传入 store
创建 Redux 切片
创建一个 Redux 切片即为在 Redux store 树 🌲 创建一个分支
文档将其写在/src/feature/counter/counterSlice.js
中
createSlice()
首先使用 React Toolkit 提供的createSlice()
创建一个 Redux 切片
1 | export const counterSlice = createSlice({ |
由这段代码可以看出createSlice
用法:
- 输入 一个对象
- name:切片名称
- initialState 初始值,文档建议为对象形式
- reducers 一个对象,包含处理更改的 reducer
- reducer 名称:reducer 函数
- 函数输入
- state:当前切片值
- action:包含 Redux dispatch 传入的原始 Action 对象,当使用 RTK 自动生成的Action Creator进行 dispatch 时,Action Creator函数的参数位于
action.payload
位置(同时这也是官方推荐的Action 对象的标准写法)
- 函数特性
- 在 Redux 中,reducer 函数应当为纯函数,因为我注意到英文文档中并没有说过他是纯函数,只是在每次提到这个问题是列举一遍其特点,这里做一下摘抄
reducers must always follow some special rules:
- They should only calculate the new state value based on the state and action arguments
- They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
- They must not do any asynchronous logic or other “side effects”
why are these rules important?
- One of the goals of Redux is to make your code predictable. When a function’s output is only calculated from the input arguments, it’s easier to understand how that code works, and to test it.
- On the other hand, if a function depends on variables outside itself, or behaves randomly, you never know what will happen when you run it.
- If a function modifies other values, including its arguments, that can change the way the application works unexpectedly. This can be a common source of bugs, such as “I updated my state, but now my UI isn’t updating when it should!”
- Some of the Redux DevTools capabilities depend on having your reducers follow these rules correctly.
- 尽管文档反复强调immutable,即不更改行参,但是由于 RTK 使用了immerjs辅助,你也可以在 RTK 的 reducer 中使用 mutating 的代码,但是请注意只能在 RTK 的 reducer 中这么写!!!
引用原文
🔥WARNING
Remember: reducer functions must always create new state values immutably, by making copies! It’s safe to call mutating functions likeArray.push()
or modify object fields likestate.someField = someValue
inside ofcreateSlice()
, because it converts those mutations into safe immutable updates internally using the Immer library, but don’t try to mutate any data outside ofcreateSlice
!
- 在 Redux 中,reducer 函数应当为纯函数,因为我注意到英文文档中并没有说过他是纯函数,只是在每次提到这个问题是列举一遍其特点,这里做一下摘抄
- 函数输入
- 也可以在 reducer 名后传入一个对象
{reducer, prepare}
prepare
函数,参数可以自定义,但与 reducer 不同的是我们可以将 reducer 禁止的代码(异步、随机、副作用等)写在prepare
函数里。使用了prepare
函数后,在其生成的Action Creator中可以直接使用我们自定义的prepare
函数的参数调用。- 参数:自定义
- 返回值:一个无需
type
字段的 Action 对象,即{payload:{}}
,也允许添加meta
、error
字段payload
:Action 对象的payload
meta
:可用于向 action 添加额外的描述性值error
:该字段应该是一个布尔值,指示此 action 是否表示某种错误
reducer
函数本身,与之前说的要求一样
- reducer 名称:reducer 函数
extraReducers
:一个函数,参数为一个builder
对象,用于为外部定义的 actions 定义 reducers,build 对象的操作可以链式调用,下面是一些用法介绍:(其中 reducer 函数与之前定义相同(state, action) => {}
)builder.addCase(actionCreator, reducer)
:为一个 Action Creator 或者一个 Action 字符串添加 reducerbuilder.addMatcher(matcher, reducer)
:为 matter function 返回为 true 的所有 action 添加 reducerbuilder.addDefaultCase(reducer)
:当没有其他项匹配时的默认 reducer样例:
1
2
3
4
5extraReducers: (builder) => {
builder
.addCase("counter/decrement", (state, action) => {})
.addCase(increment, (state, action) => {});
};
- 返回值 返回一个初始化好的 Redux 切片对象,我们可以从这个对象中获得我们有用的东西(见下文)
获取Action Creator
1 | export const { increment, decrement, incrementByAmount } = counterSlice.actions; |
此段代码用于从 Redux 切片中获取Action Creator函数
导出切片的Selectors
如果我们可以直接获取到 Redux store 对象,可以直接使用store.getState()
获取整个 store 树的存储,Selectors就是负责从整个 store 数中拿到我们切片数据的
或许我们可以直接把它(Selector)写成这样:
1 | export const selectCounterValue = (state) => state.counter; |
就是一个非常简单的把 store 树上一部分取出来的工具,可以在这里写好然后导出,也可以用的时候现写。
刚说到“如果我们可以直接获取到 Redux store 对象”,这是一种理想情况,而 RTK 不允许我们随意引用和使用 store 对象,
而且这么做也不优雅,这时候会需要用到 Redux-React 提供的useSelector
工具,将会在后文解释
导出 reducer
我们之前初始化 Redux 切片时传入的 Reducer 是一个一个独立的,还要经过 RTK 的合并和处理(其中有一项可能和之前说的 immerjs 有关),才能用于 Redux store,可以通过以下代码将 RTK 处理过的 reducer 导出
1 | export default counterSlice.reducer; |
在 React Component 中应用 Redux
这部分内容主要由Redux-React
提供,毕竟 Redux 并不是局限于某个 js 框架的状态管理工具,他是一个非常开放的工具。
获取 Redux store 数据
Redux-React
提供了useSelector
钩子用来获取 Redux store 中的数据,使用方法如下
引入
1 | import { useSelector } from "react-redux"; |
在 React Component 中调用
1 | const count = useSelector(selectCount); |
其中selectCount
即为上文中导出的 Selector。
当然也可以写为行内的 Selector:
1 | const count = useSelector((state) => state.count); |
这样可以得到 Selector 筛选后的 store 数据,并且得益于Redux-React
对 React 的单独适配,当筛选后的 store 数据变化时,对应组件也会进行重新渲染。
但是需要注意的一点是,每次调用 dispatch 时,不管在哪里调用 dispatch,都会重新计算一遍Selector
函数,如果计算结果发生变化,则会使对应组件重新渲染。这里发生的变化如果是对象,则比较的对象的引用是否发生变化,即{}!=={}
dispatch 更改
由于 Redux 禁止直接获取 store,Redux-React
提供了useDispatch
钩子用来获取 dispatch
函数
引入
1 | import { useSelector } from "react-redux"; |
在 React Component 中调用
1 | const dispatch = useDispatch(); |
然后便可以直接使用dispatch
函数去触发更改了,与store.dispatch
用法相同,可以传入Action Creator(包括 RTK 生成的 Action Creator),也可以传入一个简单的 action 对象,下面这两种方法都是可以的,传入 payload 的类型也没有限制。
1 | dispatch({ type: "counter/incrementByAmount", payload: 2 }); |
⚠️ 警告
Redux actions and state should only contain plain JS values like objects, arrays, and primitives. Don’t put class instances, functions, or other non-serializable (不可序列化) values into Redux!.
使用 Thunk 函数
thunk 函数通常写在 slice 文件内,(与createSlice
同文件),这样可以便于寻找代码。
使用简单的 Thunk
thunk 是一种特定类型的 Redux 函数,可以包含异步逻辑。Thunk 是使用两个函数编写的:
- 内部 thunk 函数,它以
dispatch
和getState
作为参数 - 外部 thunk creator 函数,它创建并返回 thunk 函数
例如在Redux Demo中有一段异步 dispatch 的例子,这个例子写在/src/feature/counter/counterSlice.js
中
1 | export const incrementAsync = (amount) => (dispatch, getState) => { |
thunk 函数可以这样 dispatch
1 | dispatch(incrementAsync(5)); |
我们可以将这个函数提高一下代码可读性来看
1 | export const incrementAsync = (amount) => { |
代入至 dispatch
1 | dispatch((dispatch, getState) => { |
也就是相当于向dispatch
传入一个函数 A,参数为dispatch
和getState
函数,在函数 A 内实现异步逻辑之后再去dispatch
。
使用 thunk 需要在创建时将
redux-thunk
middleware(一种 Redux 插件)添加到 Redux store 中。幸运的是,RTK 的 configureStore 函数已经自动为我们配置好了,所以我们可以继续在这里使用 thunk。
另外再提供一个改自文档的网络请求的例子
1 | const fetchUserById = (userId) => { |
可以看到外部 thunk creator 函数的作用是为了将我们的参数传入 thunk 内部函数,并且还能与 Action Creator 形式保持一致
使用createAsyncThunk
创建异步 Thunk
RTK 提供了一个createAsyncThunk
方法来创建异步的 Thunk,方便我们进行网络请求等操作
直接看一个文档样例
1 | export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => { |
在此例中,刚开始 fetch 数据时会 dispatch posts/fetchPosts/pending
Action,fetch 完成后会 dispatch posts/fetchPosts/fulfilled
Action,出现问题时则会 dispatch posts/fetchPosts/rejected
Action
参数
- Action 字符串
payloadCreator
函数,其中包含我们的异步代码,参数如下:arg
:dispatch 这个 Action Creator 时候传入的第一个参数,如果有多个参数,则应放在一个对象中传入thunkAPI
:一个对象dispatch
:the Redux store dispatch methodgetState
:the Redux store getState methodextra
:the “extra argument” that can be passed into the thunk middleware when creating the store. This is typically some kind of API wrapper, such as a set of functions that know how to make API calls to your application’s server and return data, so that your thunks don’t have to have all the URLs and query logic directly inside.requestId
:a unique string ID value that was automatically generated to identify this request sequencesignal
:一个AbortController.signal
对象,可用于查看执行时是否已将此请求取消rejectWithValue(value, [meta])
:rejectWithValue is a utility function that you canreturn
(orthrow
) in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action. If you also pass in ameta
, it will be merged with the existingrejectedAction.meta
.fulfillWithValue(value, meta)
:fulfillWithValue is a utility function that you canreturn
in your action creator to fulfill with a value while having the ability of adding tofulfilledAction.meta
.
options
对象condition(arg, { getState, extra } ): boolean | Promise<boolean>
:用于跳过执行payloadCreator
,详细信息Canceling Before ExecutiondispatchConditionRejection
:当上一个condition
参数返回值为false
时,不会 dispatch 任何 Action,如果想要 dispatch 一个 rejection 可以将这个参数置为true
。idGenerator(arg): string
:用于生成随机requestId
的函数。默认使用nanoidserializeError(error: unknown) => any
:to replace the internal miniSerializeError method with your own serialization logic.getPendingMeta({ arg, requestId }, { getState, extra }): any
:一个函数,用于创建一个对象最终会合入pendingAction.meta
返回值
- 一个 thunk action creator,用于 dispatch 这个异步 thunk action,eg:
'users/fetchByIdStatus'
- 还可以调用它其中包含的 action creator
pending
:待完成,eg:'users/fetchByIdStatus/pending'
fulfilled
:已完成,eg:'users/fetchByIdStatus/fulfilled'
rejected
:被拒绝,eg:'users/fetchByIdStatus/rejected'
thunk action creator 的 dispatch 过程
- dispatch the pending action
- call the payloadCreator callback and wait for the returned promise to settle
- when the promise settles:
- if the promise resolved successfully, dispatch the fulfilled action with the promise value as action.payload
- if the promise resolved with a rejectWithValue(value) return value, dispatch the rejected action with the value passed into action.payload and ‘Rejected’ as action.error.message
- if the promise failed and was not handled with rejectWithValue, dispatch the rejected action with a serialized version of the error value as action.error
- Return a fulfilled promise containing the final dispatched action (either the fulfilled or rejected action object)
关于这个createAsyncThunk
方法的具体细节可以查看https://redux-toolkit.js.org/api/createAsyncThunk
性能优化注意点
useSelector
重复渲染
问题描述
每次调用 dispatch 时,不管在哪里调用 dispatch,都会重新计算一遍Selector
函数,如果计算结果发生变化,则会使useSelector
对应组件重新渲染。这里发生的变化如果是对象,则比较的对象的引用是否发生变化,即{}!=={}
。
观察下列代码
1 | const postsForUser = useSelector((state) => { |
每次 dispatch 时计算得出的数组对象的引用是不相同的,所以会导致不管在哪里 dispatch,都会使这个组件重新渲染
解决方法
可以使用Redux Reselect创建 memoized Selector 函数。
RTK 已经帮我们加好了这个库,所以只需要在 RTK 中引出createSelector
函数即可使用它创建 Selector 函数。
1 | import { createSelector } from "@reduxjs/toolkit"; |
这里createSelector
第二个参数函数仅在第一个列表中 Selector 返回值发生变化时执行并返回新值
数据范式化
为了便于查找数据,我们可以将数据范式化(Normalized),即:
- 我们 state 中的每个特定数据只有一个副本,不存在重复。
- 已范式化的数据保存在查找表中,其中项目 ID 是键,项本身是值。
- 也可能有一个特定项用于保存所有 ID 的数组。
JavaScript 对象可以用作查找表,类似于其他语言中的 “maps” 或 “dictionaries”。 以下是一组“用户”对象的范式化 state 可能如下所示:
1 | { |
For more details on why normalizing state is useful, see Normalizing State Shape and the Redux Toolkit Usage Guide section on Managing Normalized Data.
使用createEntityAdapter
管理范式化数据
RTK 提供了createEntityAdapter
API 来管理范式化数据,可以将数据以{ ids: [], entities: {} }
形式存储,并且生成一系列 reducer 和 selector ,它有以下优点:
- 不用自己手动维护序列化数据
- 预先生成一系列 reducer:添加所有项、更新一个项、删除多项 等
- 可以将项 ID 按一定顺序排列,并且仅当项增删改或者排序规则改变时会重新排列
createEntityAdapter
:
接收参数(一个配置对象):
sortComparer
函数:ID 排序函数(排序函数写法类似Array.sort()
)
返回值(一个adapter
对象):
- 包含一系列用于增删改的 reducer,具体 reducer 参见createEntityAdapter#crud-functions
getSelectors
函数- 接收一个 Selector 函数,用于从 store 中筛选出序列化数据所在位置
eg:state => state.posts
- 返回一个对象,包含一系列序列化数据选择器:
selectAll
selectById
selectIds
- 接收一个 Selector 函数,用于从 store 中筛选出序列化数据所在位置
getInitialState
函数:将会返回这组序列化数据的 InitialState,用于填入 RTKcreateSlice
中的initialState
,除了序列化数据原有的值{ ids: [], entities: {} }
外,如果想向里面加入其他键值,可以以对象形式传入这个函数:const initialState = postsAdapter.getInitialState({ status: 'idle', error: null })
经过createEntityAdapter
生成的对应 state 形式为{ ids: [], entities: {} }
,ids
用于存键名列表,entities
对象用于键值对应数据
其他功能
@reduxjs/toolkit
nanoid
基于nanoid/nonsecure,用于生成一个随机 id 字符串,在 Redux 内部主要用于管理createAsyncThunk
的异步请求 id,也可以用于其他用途。
1 | import { nanoid } from "@reduxjs/toolkit"; |