先看下这张思维导图清晰地过一下本篇文章的结构:
目录
为什么要用 redux-tookit?
要说为什么要用 Redux-Tookit,先想给大家讲一下 Redux。
Redux 的基本概念
Redux 的诞生
因为 React 本身是一个构建用户界面的轻量级的 JS 库,只是 DOM 的一个抽象层,组件内部数据也是需要遵循单向数据流的设计原则,而且当需要处理大量的数据或者项目内需要共享的数据,对它来说不是它的本职,以致单独使用 React 并不是 Web 应用的完整解决方案。
一个简单的 React 组件 🌰:
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}

单个组件内的渲染流程
想只用 react 来编写一个复杂的大型项目,组件之间传值就很麻烦,所以可以结合一个数据层的框架来与 react 进行配套使用,这样一来呢就可以 hold 住大型的项目了。
Redux 就是这样一个数据层框架,简述一下:
Redux 可以在同一个地方查询状态、改变状态、传播状态的变化,其要求我们把数据都存在一个叫做 store 的公共仓库中,组件中基本没有数据(当然一些组件内部还是可以拥有自身的数据的,Redux store 按需存放),然后 store 中的数据被修改时,用到 store 中数据的组件都会响应到并拿最新的值去渲染,从而间接地实现了数据跨组件传递。
Redux 的设计思想
-
Web 应用是一个状态机,视图与状态是一一对应的;
-
所有的状态,保存在一个叫 store 的公共仓库中。
Redux 的工作流程
相信下面这张图,大家在学习 redux 的文档中没少看到过,用比较容易理解的例子给大家解释一下下面这张图:

场景是 -> 我想去图书馆借一本书,到图书馆在前台告知图书管理员想借什么书,图书管理员肯定不知道这书有没有/在什么地方,ta 也需要通过图书管理系统进行查询,然后用查询到的结果帮助你借书,以此为背景给大家明确几个概念:
React Components-> 借书人
Action Creators -> 借什么书
dispatch(action) -> 告知图书馆管理员借书的动作
Store -> 图书馆管理员
Reducer -> 图书馆图书管理系统
第一次借书:
借书人(React Component)通过 dispatch 告知图书管理员(Store)自己想借的书(Action Creators),管理员(Store)收到借书信息(Action Creators)后,previousState 默认为 initialState ,把信息传递给图书管理系统,让其进行查询,之后管理系统返回想借的书最新信息(newState),借书人可根据信息完成借书操作;
第二次还书+借书
借书人(React Component)通过 dispatch 告知自己想借/还的书(Action Creators),管理员(Store)收到借/还书信息后,把信息传递给图书管理系统,之后管理系统拿到还书的旧数据(previousState),并返回想借的书的信息(新数据 newState),借书人可根据信息完成还书/借书操作。

store 是个仓库,将接受到的信息传递给 reducers 进行处理,然后将处理后的新的数据返回给仓库,数据改变了,也可以直接作用到 react component 上,因为 react 帮助我们监听了组件中的数据,一旦数据发生改变,就会 re-render 组件。
类比上述的例子,我们还可以再举一个例子,比如去酒店订/退房间,订房人/酒店前台/酒店的房间管理系统之间也是这样的一个流程:订房人发起一个订房动作,酒店前台收到后,拿着订房信息去管理系统进行查询,并将查询后的数据返回,订房人可根据信息订/退房间。
官网的这两个动图也能很好地帮助理解 redux 的流程:

redux 同步

redux 异步
从上述的两个通俗的例子中可以看出,多个借书/订房人之间的数据想要互通需要这样的一个数据管理系统进行辅助,而 React 的设计本身不太好帮我们做这样的一件事,所以 Redux 数据层框架应运而生。
Redux 可能适用的使用场景
从组件角度看,如果你的应用有以下场景,可以考虑使用 Redux:
-
某个组件的状态,需要共享
-
某个状态需要在任何地方都可以拿到
-
一个组件需要改变全局状态
-
一个组件需要改变另一个组件的状态
一些 redux 概念的补充和参考:
https://redux.js.org/
https://blog.csdn.net/Hazel928/article/details/113847280
https://blog.csdn.net/qq_37279880/article/details/106281059
为什么用 Redux-Tookit
说了这么多,你只告诉我什么是 Redux ,为什么要用 Redux-Tookit 并没有说啊,别急,把这些说清楚之后,我们再看原因就好理解一些了。
首先看下官方建议:

由于 Redux 的各种规范与插件异常繁琐,所以官方推出了 Redux-Toolkit 这个库来简化 Redux 的使用,简化代码,排除常规的 Redux 错误或 bug。
再看下 redux 的 todo 示例:
const ADD_TODO = 'ADD_TODO' // action type constants
const TODO_TOGGLED = 'TODO_TOGGLED' // action type constants
// hand-written action creators
export const addTodo = text => ({
type: ADD_TODO,
payload: { text, id: nanoid() }
})
// hand-written action creators
export const todoToggled = id => ({
type: TODO_TOGGLED,
payload: id
})
export const todosReducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return state.concat({ id: action.payload.id, text: action.payload.text, completed: false })
case TODO_TOGGLED:
return state.map(todo => {
if (todo.id !== action.payload.id) return todo
return { ...todo, completed: !todo.completed }
})
default:
return state
}
}
这里的 reducer 函数就只是一个函数,你需要手动去写 action type constants,手写 action creators,还要有一个 switch 根据一堆的 action type 写 case 来手动更新 state(手动更新 state ,很容易将不可变的 state 直接更改)。在一些逻辑比较复杂的场景里,你可能需要将 action type constants ,action creators,以及 reducers 抽离到各种constants/todos.js, actions/todos.js, 和reducers/todos.js文件中,甚至这三类文件也会有多个,代码散落的到处都是,不易阅读不易管理,手写代码很多,容易出现 bug。
最后我们看下 redux-tookit 的 todo:
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push({ id: action.payload.id, text: action.payload.text, completed: false })
},
todoToggled(state, action) { const todo = state.find(todo => todo.id === action.payload) todo.completed = !todo.completed } }
})
export const { todoAdded, todoToggled } = todosSlice.actions
export default todosSlice.reducer
所有的 action creators 和 action types 都是自动生成的,不需要再手写一堆的 action creators 和 action types,而且 reducer 的代码也更简短更加容易理解,在每个 reducer 里也能更加清晰地知道干了些什么,更新哪些数据。细心的小伙伴也许也发现了,为什么在 redux-tookit 里可以直接操作 state,而不害怕出现问题呢,因为 redux-tookit 集成了 immer,这个库可以让我们直接修改 state,并帮助我们生成一个新的不可变的 state,但却不修改老的 state,极大地减少了我们在处理 state 的时候容易修改老的 state 的问题,让我们的 js 操作数据的习惯可以直接无缝衔接。
由于 Redux 是几周内的产物,外加许多开发者在使用 Redux 的过程中确实略感笨重,所以 React 团队出了 Redux-Tookit 这样一个工具,来帮助我们简化整个流程。基于官方团队的强烈推荐,下面我们就开始了解一下 Redux-Tookit。
Redux-Tookit 原理及实践
Redux-Tookit 概述
Redux Toolkit(也称为“RTK”)是 Redux官方推荐开箱即用的高效 Redux 开发工具集。
它最初是为了帮助解决有关 Redux 的三个常见问题而创建的:
-
"配置 Redux Store 过于复杂"
-
"我必须添加很多软件包才能开始使用 Redux 做事情"
-
"Redux 有太多样板代码"(指那些 action creators / action types / switch case 语句)
@reduxjs/tookit 围绕核心 Redux 包,包括几个实用程序功能,这些功能可以简化最常见场景下的 Redux 开发,包括配置 store、定义 reducer,不可变的更新逻辑、甚至可以立即创建整个状态的 “切片 slice”,而无需手动编写任何 action creator 或者 action type。它还包括使用最广泛的 Redux 插件,例如 Redux Thunk 用于异步逻辑,而 Reselect 用于编写选择器 selector 函数,简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。
Redux Toolkit 包含:
-
configureStore:提供简化的配置选项。它可以自动组合切片 slice 的 reducer,添加你提供的任何 Redux 中间件,默认情况下包含redux-thunk,并启用 Redux DevTools 扩展; -
createReducer: 它允许我们为 case reducer 函数提供一个 action type 查找表,而不是编写 switch 语句。此外,它还自动使用immer库,让您使用普通的可变代码来编写更简单的不可变更新,比如 state.todos [3].complete = true。 -
createAction: 为给定的 action type 字符串生成 action creator 函数。函数本身定义了toString (),因此可以使用它来代替类型常量。 -
createSlice:接受一个 slice 切片名和初始状态 initial state 以及一组 reducer 函数,在内部是使用了createReducer和createAction来实现,从而可以帮我们自动生成具有相应 action creator 和 action type 的 slice reducer; -
createAsyncThunk: 接受一个 action type 的字符串和一个返回 promise 的函数作为参数,并生成一个thunk,该 thunk 基于这个 promise 可以 dispatchpending/fulfilled/rejected三种状态的 action; -
createSelector 工具来源于 Reselect 库,Redux-Tookit 重新 export 出来以方便使用。 -
createEntityAdapter: 生成一组可重用的 reducers 和 selectors,以管理 store 中的规范化数据;
以上API,光看概念可能不是很好理解,下面让我们来结合实例进行分析。
Redux-Tookit 的实际运用
创建 Store
redux 版:


注意这里的创建分了几步:
-
结合所有的切片 reducers 形成 root reducer;
-
在 store 文件中引入 root reducer;
-
并引入 thunk 中间件,
applyMiddleware和composeWithDevToolsAPIs; -
使用中间件和 devtools 来扩展 store
-
用 root reducer 和 enhancer 来创建 store
使用 configureStore
redux-tookit 的 configureStore API 可以很好地帮助我们简化 store 的创建流程。
configureStore 包含了 Redux core createStore API ,可以自动帮我们处理几乎所有的 store 创建流程。事实上,我们可以有效地将上述的创建流程缩短至一步:

让我们看下 configureStore 为我们做了哪些工作:
-
首先将我们创建的
todosReducer和filtersReducer结合进 root reducer 函数,该函数会将 root state 处理成{todos, filters}的形式; -
用上步的 root reducer 创建一个 Redux store;
-
自动添加
thunk中间件; -
它自动添加更多中间件来检查常见错误,例如意外改变状态;
-
它会自动设置 Redux DevTools Extension 连接,即自动扩展 DevTools。
尝试打开这个 todo 的实例并进行测试,可以看到现有的功能代码运行良好。我们也需要当我们 dispatch action,dispatch thunks(如一些异步操作),在 UI 中读取 state 状态,以及在 DevTools 中看 action 历史时,所有的这些功能都要能正常运行。
现在让我们看下如果意外改变了一些 state 状态时会如何。在 "todos loading" reducer 中,直接改变 state 的值,而不是复制出一个不可变的副本(不可变理解:不直接改变上一个 state 的状态,而是创建一个副本来返回一个新的 state,保留原 state)

不好,我们整个 APP 挂掉了,发生了什么?

这个报错提示可以告知我们,在我们的 APP 中遇到了一个 bug,configureStore 特定添加了一个额外的中间件,当其遇到意外修改了 state 的情况时会自动抛出上述这样的错误(仅在 development 模式下)。这可以帮助我们在编写代码时,捕捉到可能会产生的错误。那这样直接在 Redux 中改变 state 会出现问题,让我们看看 Redux-Tookit 有没有什么办法能够很好地帮助我们解决这个问题。
写 Slices
每当我们在 app 中添加一些新的功能时,slice 文件会变得越来越大,越来越复杂和笨重。尤其是 todosReducer 变得更加难以阅读,因为所有嵌套对象都是不可变更新,那在更新时代码量会很大,而且我们会写多个 action creator 函数。
Redux Toolkit 有一个 createSlice API ,可以帮助简化我们的 Redux reducer 逻辑和 action 的创建。 createSlice能够为我们做以下几项重要的事:
-
我们可以在一个对象里写 case reducers,其会被作为一个函数,来替代之前的 switch/case;
-
在 reducers 里可以写更短的不可变更新的逻辑(因为结合了 immer ,所以可以直接简化为对 state 的更改);
-
会根据我们提供的 reducers 函数,来帮我们自动生成 action creators。
使用createSlice
createSlice 接收一个拥有三个主要属性的对象作为参数:
-
name: 字符串,将用来作为生成的 action type 的前缀; -
initialState: reducer 的初始化 state 对象; -
reducers: 一个对象,键是字符串,对应的值是处理特定(同步) action 的 'case reducer' 函数; -
extraReducers: 这个属性是一个回调函数,builder作为该函数的一个参数,用于处理异步或特定 action 的 'case reducer' 函数。 builder 对象下有三个不同的方法,分别用于处理不同情况下的 action:-
builder.addCase
添加一个用于处理单独明确的 action type 的 case reducer,如获取某个列表 fetchList 数据的 action,来处理fetchList.pending,fetchList.fufilled,fetchList.rejected(下一节的 异步的 thunk 和 action 中有结合实例来介绍其用法)。参数:2) reducer: 针对某个 action 的 reducer 方法。 -
builder.addMatcher
不针对某一个特定的 action.type 属性,而是可以匹配所有用方法过滤出的 actions。如果给定的 matcher 匹配到多个 actions,那么这些匹配的 actions 将会按他们定义的顺序执行。参数:1) matcher:一个用于匹配 action 的函数,在 ts 中,需要是一个指定类型的函数;
2) reducer:针对匹配到的 action 要执行的 reducer 方法。


-
builder.addDefaultCase
定义一个'default case' reducer,当某个 action 没有匹配的 reducer 时,就可以执行这个默认的 reducer 内容 。
参数:
reducer:默认为没匹配到 recuder 的 action 要执行的方法。
需要注意的是builder.addCase的调用均在builder.addMatcherorbuilder.addDefaultCase之前,builder.addMatcher的调用在builder.addDefaultCase之前。
详细内容可参考官网:https://redux-toolkit.js.org/api/createReducer#usage-with-the-builder-callback-notation
-
现在让我们看一个小的 createSlice独立示例。

在这个例子里可以看到以下几点:
-
我们在
reducers对象里写 case reducer 函数,并给他们定义可读性较强的名称(这个大家在开发过程中也要注意,函数名/变量名的高可读性也是能够使代码更加健壮的重要基础之一); -
createSlice会对应我们提供的每个 case reducer 函数,来自动生成对应的 action creators; -
createSlice在默认情况下自动返回现有状态(不做任何更改时的初始化状态); -
createSlice允许我们安全地'mutate' 我们的状态(因为对原时间节点上的状态有所保留,可追溯,所以直接修改也没问题,如 todoAdded); -
不过,如果我们愿意,依然是可以在更改状态前创建一个不可变的副本(like todosLoading reducer)。
自动生成的 action creators 可以作为 slice.actions.todoAdded 来获取和使用,我们通常像我们之前编写的 action creators 一样单独解构和导出它们。完整的 reducer 函数,会被作为 slice.reducer,我们也会跟之前一样export default slice.reducer,默认导出 slice.reducer。
那自动生成的 action 对象会是什么样?让我们手动调用上述其中一个 action 并进行打印看看:

createSlice 通过将 slice 的 name(命名空间)字段和我们在 reducers 对象中写的 reducer 函数的名称 todoToggled 相结合的形式,为我们生成 action type。默认情况下,action creator 接受一个参数,并将其作为 action.payload 放入操作对象中。
在生成的 reducer 函数内部,createSlice 将检查派发 action 的 action.type 是否与它生成的名称之一匹配。如果是这样,它将运行那个 case reducer 函数。这与我们使用 switch/case 语句编写的模式完全相同,但 createSlice 会自动为我们完成。
有关 'mutation' 这一方面也值得仔细地看下更多细节。
Immutable Updates with Immer
早前,我们谈论到 "mutation"(修改存在的 object/array 的值)和 "immutability" (对待一个不可改变的值)。
在 Redux 中,reducers 是绝对不会允许改变 原始的/当前的 state 的值的!

所以如果我们不能修改原始值(原始状态),那我们要如何返回一个新的 state?Redux 中只能通过对原始 state 创建副本,对副本进行修改,从而返回一个新的 state。(个人理解的是,对于当前的这个原始状态,react 和 redux 是需要利用这个原始状态进行追溯的,不能对这个原始状态进行修改,只能将原始状态进行复制后再改变,返回新的被更改后的状态,等到下次派发 action,上次返回的被更改的状态又会作为下次的初始状态。这是一整个状态的缓存,每一个节点上的 state 状态都是独立的,不可更改的。)

正如在本次分享中提到的那样,我们可以通过使用 JavaScript 的数组/对象扩展运算符和其他返回原始值副本的函数来手动编写不可变的更新。但是,手动编写不可变的更新逻辑很困难,并且意外地改变reducers 中的状态是 Redux 用户最常犯的错误。
这也是为什么 Redux Toolkit's createSlice 函数可以让你用一种更简单轻松的方式编写不可变的更新。
createSlice 内部使用了 Immer 库。Immer 使用一种称为 Proxy 的特殊 JS 工具来包装您提供的数据,并允许您编写代码来“改变”包装的数据。但是,Immer 会跟踪您尝试进行的所有更改,然后使用该更改列表返回一个安全、不可变的更新后的值,就像您手动编写了所有不可变更新逻辑一样。具体上的实现是当我们对 state 进行修改,proxy对象会拦截,并且按顺序替换上层对象,返回新对象。看上去就好像自动帮我们直接修改了state。
所以,不是这个:

而是你可以像下面这样编写状态的修改:

这样代码就易读许多。
但是还有很重要的几点需要注意:

Immer 仍允许我们手写不可变的更新并返回一个新的状态值。我们也可以混合,比如从一个数组中移除一条,通常使用array.filter() 会容易得多,所以你可以调用 array.filter(),然后将返回值赋值给 state 来改变 state:

creatSlice - reducer 如何传递多个参数
还是看我们多次举的 todo 的 slice 文件的例子:



上面的 reduder 中,action.payload 可以是一个 todo 对象,也可以是一个需要使用的 todoID,我们需要意识到,todoAdded 和 todoToggled 都只需要传入一个参数,但如果我们需要传入两个参数进行一些逻辑处理时,应该要如何进行?

如上图中的 todoColorSelected ,createSlice 让我们在 reducer 中添加 'prepare callback' 来处理需要多个参数的情况。我们可以传递一个拥有reducer 和 prepare 的对象。当我们调用生成的 action creator时,prepare 函数会带着传入的任何参数被调用。prepare 被调用之后,会创建和返回一个拥有 payload 属性的对象(或者(可选)的meta and error字段),与 Flux Standard Action convention 相匹配。
这里我们用了一个prepare 回调,使我们的todoColorSelected action creator 接受分开的 todoId 和 color 参数,并将他们作为一个对象放在action.payload 中(也就是在派发todoColorSelected 这个 action 的时候,可以传两个参数,prepare 会将这两个参数进行整合,并返回一个 ation 参数对象,通过 action.payload 可以解构出传入的参数,是需要传入多个参数的解决方案)。
同时,在todoDeleted reducer中,我们可以使用JS delete 运算符从规范化状态中删除项目。
以上是 createSlice 的介绍和使用,这里创建的都是项目里能同步修改 state 的 action,那如果我们需要调用接口,异步修改我们的 state,又要如何处理呢?
异步的 thunk 和 action
首先我们看下 redux 如何来写异步的 thunk ->
src/features/todos/todosSlice.js:

依然需要手动去写 action creators 。
Redux Toolkit 有createAsyncThunk 这样一个 API,可以帮助我们生成 thunk,也会为不同的请求状态生成 action types 和 action creators,这个 API 返回一个 Promise,createAsyncThunk 还会基于这个返回值自动派发这些 action 。
使用 createAsyncThunk
让我们来用生成一个 thunk 的 createAsyncThunk 方法来改写上面的 fetchTodos thunk ->
createAsyncThunk 接受两个参数:
-
一个将会被用来作为生成的 action types 前缀的字符串;
-
一个会返回 Promise 的 'payload creator' 回调函数(是一个利用接口返回值创建负载并返回一个 Promise 的函数)。这里写的时候通常使用
async/await语法,因为asyncfunction 会自动返回一个 Promise。

我们传递 'todos/fetchTodos' 作为字符串前缀(注意这里的 todos 是 slice 的 name,fetchTodos 是该 thunk 要做的事情,后续生成的三种状态的 action 也是基于 fetchTodos 命名),"payload creator" 函数调用我们的 API 接口,并返回一个包含接口返回值的 Promise。在内部,createAsyncThunk 会生成三个不同的 action creators 和对应的 action types,还生成一个 thunk 函数,该函数在调用时自动派发这些 actions。在上述的 fetchTodos 中,这些 action creators 和他们对应的 types 分别是:
-
fetchTodos.pending:todos/fetchTodos/pending -
fetchTodos.fulfilled:todos/fetchTodos/fulfilled -
fetchTodos.rejected:todos/fetchTodos/rejected
然 🦢,这些 action creators 和 types 都是在 createSlice 调用之外被定义的。我们不能在createSlice.reducers 的作用域内处理这些 actions,因为在 reducers 中也会生成新的 action types。我们需要这样一种方式,当 createSlice 调用时,能够监听到该函数外部定义的其他的 action types。
所以,这样的一种方式就有了:createSlice 的参数对象中也接受一个 extraReducers 属性,在extraReducers 中我们可以在同一个 slice reducer 中监听和处理其他的 action types。这个属性是一个回调函数, builder作为该函数的一个参数,然后我们就可以调用builder.addCase(actionCreator, caseReducer) 来监听非 createSlice 内部声明的 actions 了。
回到刚才的 fetchTodos,可以看到我们调用了 builder.addCase(fetchTodos.pending, caseReducer)。当 fetchTodos.pending 这个 action 被派发,我们会运行对应的 reducer ,即设置 state.status = 'loading',在这里就与我们之前用 switch 语句写的逻辑相同 -> 改变 state 。我们也可以对 fetchTodos.rejected 做同样的 action 监听,从而处理从接口返回的数据。
以上是 createAsyncThunk 的基本应用,还有以下一些详细应用可以作为参考和扩展:
-
当你 dispatch 一个 thunk 时,只能传递一个参数,如果想传递多个参数,可以将这些参数放入一个对象中;
-
payload creator 函数接收一个对象作为第二个参数,这个对象里包含了许多有用的方法,如
{getState, dispatch,rejectWithValue}等等,具体释义可参考官网:https://redux-toolkit.js.org/api/createAsyncThunk
实际项目中 getState 和 rejectWithValue 的应用:

-
Thunk 会在执行 payload creator 之前先派发
pendingaction,然后是派发fulfilledaction 还是rejectedaction 取决于Promise返回的是成功还是失败。
createAsyncThunk 官网地址 -> https://redux-toolkit.js.org/api/createAsyncThunk
createSelector 介绍和使用
createSelector 简介
我们知道 store 中会存储许多不同类型的数据,如何快速地获取指定状态的信息(数据)是我们在项目中比较常见的应用场景,createSelector 这个API 通过对 store 中 state 的读取和筛选来生成指定的状态选择器,这样我们在使用到某一个state tree 下的某个变量的值时,就可以通过该变量的选择器直接进行获取,可以对某个对象生成一个选择器,也可以对某个单独的变量生成选择器。
createSelector功能来自 Reselect library,Redux-Tookit 为方便使用重新导出的,让我们去 Reselect 库了解下详细内容。
Reselect 导出 createSelector API,用于生成记忆 selector 函数 。createSelector 接受一个或多个'input' 选择器,从参数中提取值,以及接收提取值并返回派生值的“输出”selector。input 和 选择器的结果都会被缓存起来待之后使用。如果这个生成的新的 selector 以相同的 arguments 被调用,那么前一个缓存的结果会被直接返回,而不是再重新计算返回一个新值(这也就是前面提到的记忆 selector 函数的功能)。
使用 createSelector

默认情况下,使用 createSelector 创建的选择器的缓存大小为 1。这意味着当 input-selector 的值发生变化时,它们总是重新计算,因为选择器只存储每个 input-selector 的上一个值。这可以通过传递一个 selectorOptions 对象来自定义,该对象具有一个 memoizeOptions 字段,该字段包含内置 defaultMemoize记忆函数的选项。

其中 defaultMemoize :
-
equalityCheck: 用于比较所提供计算函数的各个参数 -
resultEqualityCheck: (如果提供),用于将新生成的输出值与缓存中的先前值进行比较。如果找到匹配项,则返回旧值。 -
maxSize: 选择器的缓存大小。如果 maxSize 大于 1,则选择器将在内部使用 LRU 缓存(作为了解:LRU 缓存 -> https://blog.51cto.com/lxw1844912514/3078701)
Normalizing State(state 规范化)
我们之前看到过如何在一个对象中通过 item IDs 作为每个 item 的唯一标识来“规范化” state 的。这让我们能够通过 ID 去查询某一个 item,而不是通过遍历整个数组去进行查询。然 🦢,手写逻辑去更新规范化的 state 依然是冗长且乏味的。用 Immer 对 state 写可变的更新代码让逻辑更简单了些,但仍然可能会有大量的重复——在我们的 app 中,可能会加载许多不同类型的 items,这样的话我们每次都必须重复相同的 reducer 逻辑去 mutate state。
Redux Toolkit 提供了一个 createEntityAdapter API ,该 API 预设了一些 reducers,用于具有规范化状态的典型数据的更新操作。包含了从 slice 中添加, 更新以及移除 items。createEntityAdapter 还生成一些了memoized selectors,用于从 store 中读取值。
使用 createEntityAdapter
函数的参数是一个对象,里面是两个可选字段:
-
selectId: 一个参数为单个Entity实例的函数,返回其中任何唯一 ID/标识 字段的值。如果没有提供该字段,则默认会执行entity => entity.id。如果Entity实例中每项的唯一标识不是entity.id而是其他字段,此时就必须要提供一个selectId函数; -
sortComparer: 一个参数为两个Entity实例的函数,返回一个标准的Array.sort()数值结果(1, 0, -1),用于控制 'all IDs' 数组的相对排序顺序。

接着往下看,当调用 createEntityAdapter API 时,会返回一个 "adapter" 对象,该对象包含多个预设的 reducer 函数,分别有:
-
addOne/addMany: 向 state 中添加一个 item / 多个 items; -
upsertOne/upsertMany: 添加新 items 或更新已经存在的 items; -
updateOne/updateMany: 通过提供部分值来更新现有 items; -
removeOne/removeMany: 基于 IDs 来删除 items; -
setAll: 替换所有现有 items。
我们可以将这些 funtions 作为 case reducers 或 "mutating helpers" 在 createSlice 中使用。
adapter 对象还包含:
-
getInitialState: 返回一个类似{ ids: [], entities: {} }的对象,用来对规范化的 state 以及所有 item IDs 组成的数组进行存储; -
getSelectors: 生成一组标准的 selector 函数。
看下如何在 todosSlice 中使用这些方法:


不同的 adapter reducer 函数,根据功能采用不同的值作为参数,但都是接受派发 action 时 action.payload 的值。'add' 和 'upsert' 函数会接受一个单独的 item 或一个 items 数组,'remove' 函数接受一个单独的 ID 或由 IDs 组成的数组,其他的函数依此类推。
getInitialState 允许我们传入将包含在内的其他 state 字段。在这个例子中,我们传入 status 字段,最终会给我们 一个{ids, entities, status} todosSlice state,就像我们之前声明的那样。
我们也可以将之前 todos selector 进行改写。
数据结构应如下:
{
todos: { ids: [],
entities: { 1: {}, 2: {}, } }
}
之前的 todo selector :
const selectTodoEntities = (state) => state.todos.entities
export const selectTodos = createSelector(selectTodoEntities, (entities) => Object.values(entities)
export const selectTodoById = (state, todoId) => {
return selectTodoEntities(state)[todoId]
}
export const selectTodoIds = createSelector(
// First, pass one or more "input selector" functions:
selectTodos, // [{}, {}], 由todos组成的数组
// Then, an "output selector" that receives all the input results as arguments
// and returns a final result value
(todos) => todos.map((todo) => todo.id) // 返回有每个 todo 的 id 组成的数组
)
// 过滤出符合条件的 todos
export const selectFilteredTodos = createSelector(
// First input selector: all todos
selectTodos,
// Second input selector: all filter values
(state) => state.filters, // 获取 state 中存储的 filters,用于过滤 todos
// Output selector: receives both values
(todos, filters) => {
const { status, colors } = filters
const showAllCompletions = status === StatusFilters.All
if (showAllCompletions && colors.length === 0) {
return todos
}
const completedStatus = status === StatusFilters.Completed
// Return either active or completed todos based on filter
return todos.filter((todo) => {
const statusMatches = showAllCompletions || todo.completed === completedStatus
const colorMatches = colors.length === 0 || colors.includes(todo.color)
return statusMatches && colorMatches
})
})
// 利用过滤出的 todos, 再获取这些 todos 的 id
export const selectFilteredTodoIds = createSelector(
// Pass our other memoized selector as an input
selectFilteredTodos,
// And derive data in the output selector
(filteredTodos) => filteredTodos.map((todo) => todo.id)
)
adapter 对象的 getSelectors 方法,会生成两个 selectors ,其中有selectAll,会返回一个所有 items 的数组,selectById 会根据传入的 ID 返回对应的 item。然 🦢,因为getSelectors 并不清楚我们需要的数据处于整个 Redux state tree 的哪个位置,所以需要我们传入一个小的 selector,用于从整个状态树中返回我们需要的 state。


可以看出使用 adapter 对象的 getSelectors 这一行代码,可以直接实现上述 selectTodoEntities,selectTodos,selectTodoById 三个 selector。
createEntityAdapter官网地址 -> https://redux-toolkit.js.org/api/createEntityAdapter

4264

被折叠的 条评论
为什么被折叠?



