跳至主要内容

Redux 基础知识,第 8 部分:使用 Redux Toolkit 的现代 Redux

您将学到什么
  • 如何使用 Redux Toolkit 简化您的 Redux 逻辑
  • 学习和使用 Redux 的下一步

恭喜你,你已经完成了本教程的最后一部分!在结束之前,我们还有一个主题要讲。

如果你想回顾一下我们之前学过的内容,可以看看这个总结

信息

回顾:你学到了什么

  • 第一部分:概述:
    • 什么是 Redux,何时/为何使用它,以及 Redux 应用程序的基本组成部分
  • 第二部分:概念和数据流:
    • Redux 如何使用“单向数据流”模式
  • 第三部分:状态、动作和 Reducer:
    • Redux 状态由纯 JS 数据组成
    • 动作是描述应用程序中“发生了什么”事件的对象
    • Reducer 接收当前状态和一个动作,并计算一个新的状态
    • Reducer 必须遵循“不可变更新”和“无副作用”等规则
  • 第四部分:Store:
    • createStore API 使用根 Reducer 函数创建一个 Redux store
    • Store 可以使用“增强器”和“中间件”进行自定义
    • Redux DevTools 扩展程序允许你查看状态随时间的变化
  • 第五部分:UI 和 React:
    • Redux 与任何 UI 都分离,但经常与 React 一起使用
    • React-Redux 提供 API,让 React 组件可以与 Redux store 交互
    • useSelector 从 Redux 状态读取值并订阅更新
    • useDispatch 允许组件分派动作
    • <Provider> 包裹你的应用程序,并允许组件访问 store
  • 第六部分:异步逻辑和数据获取:
    • Redux 中间件允许编写具有副作用的逻辑
    • 中间件在 Redux 数据流中添加一个额外的步骤,从而实现异步逻辑
    • Redux “thunk” 函数是编写基本异步逻辑的标准方法
  • 第七部分:标准 Redux 模式:
    • 动作创建者封装了准备动作对象和 thunk 的过程
    • 记忆化的选择器优化了计算转换后的数据的过程
    • 请求状态应使用加载状态枚举值进行跟踪
    • 规范化状态使通过 ID 查找项目变得更容易

正如您所见,Redux 的许多方面都涉及编写一些可能冗长的代码,例如不可变更新、操作类型和操作创建者以及规范化状态。这些模式存在是有充分理由的,但“手动”编写这些代码可能很困难。此外,设置 Redux 存储的过程需要几个步骤,我们不得不为诸如在 thunk 中调度“加载”操作或处理规范化数据之类的事情想出自己的逻辑。最后,许多时候用户不确定编写 Redux 逻辑的“正确方法”是什么。

这就是 Redux 团队创建 Redux Toolkit:我们官方的、有见地的、“包含电池”的工具集,用于高效的 Redux 开发 的原因。

Redux Toolkit 包含我们认为对构建 Redux 应用程序至关重要的包和函数。Redux Toolkit 构建了我们建议的最佳实践,简化了大多数 Redux 任务,防止了常见的错误,并使编写 Redux 应用程序变得更容易。

因此,Redux Toolkit 是编写 Redux 应用程序逻辑的标准方法。您在本教程中到目前为止编写的“手写”Redux 逻辑是实际的工作代码,但您不应该手动编写 Redux 逻辑 - 我们在本教程中介绍了这些方法,以便您了解 Redux 的工作原理。但是,对于实际应用程序,您应该使用 Redux Toolkit 来编写您的 Redux 逻辑。

当您使用 Redux Toolkit 时,我们到目前为止介绍的所有概念(操作、reducer、存储设置、操作创建者、thunk 等)仍然存在,但Redux Toolkit 提供了更简单的方法来编写这些代码

提示

Redux Toolkit 涵盖 Redux 逻辑 - 我们仍然使用 React-Redux 让我们的 React 组件与 Redux 存储进行通信,包括 useSelectoruseDispatch

因此,让我们看看如何使用 Redux Toolkit 来简化我们在示例待办事项应用程序中已经编写的代码。我们主要会重写我们的“切片”文件,但我们应该能够保持所有 UI 代码不变。

在我们继续之前,将 Redux Toolkit 包添加到您的应用程序中

npm install @reduxjs/toolkit

存储设置

我们已经对 Redux 存储的设置逻辑进行了多次迭代。目前,它看起来像这样

src/rootReducer.js
import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})

export default rootReducer
src/store.js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

const store = createStore(rootReducer, composedEnhancer)
export default store

请注意,设置过程需要几个步骤。我们必须

  • 将切片 reducer 组合在一起,形成根 reducer
  • 将根 reducer 导入到 store 文件中
  • 导入 thunk 中间件、applyMiddlewarecomposeWithDevTools API
  • 使用中间件和 devtools 创建一个 store 增强器
  • 使用根 reducer 创建 store

如果我们能减少这里的步骤数量,那就太好了。

使用 configureStore

Redux Toolkit 有一个 configureStore API,它简化了 store 设置过程configureStore 围绕 Redux 核心 createStore API 进行包装,并自动为我们处理大多数 store 设置。实际上,我们可以将其简化为一步

src/store.js
import { configureStore } from '@reduxjs/toolkit'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
reducer: {
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
}
})

export default store

configureStore 的一次调用就完成了所有工作

  • 它将 todosReducerfiltersReducer 组合成根 reducer 函数,该函数将处理看起来像 {todos, filters} 的根状态
  • 它使用该根 reducer 创建了一个 Redux store
  • 它自动添加了 thunk 中间件
  • 它自动添加了更多中间件来检查常见的错误,例如意外地修改状态
  • 它自动设置了 Redux DevTools Extension 连接

我们可以通过打开我们的示例 todo 应用程序并使用它来确认这一点。我们所有现有的功能代码都正常工作!由于我们正在分派操作、分派 thunk、在 UI 中读取状态以及查看 DevTools 中的操作历史记录,因此所有这些部分都必须正常工作。我们所做的只是更换了 store 设置代码。

让我们看看现在如果我们意外地修改了一些状态会发生什么。如果我们更改“todos 加载”reducer,使其直接更改状态字段,而不是不可变地创建副本,会发生什么?

src/features/todos/todosSlice
export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other cases
case 'todos/todosLoading': {
// ❌ WARNING: example only - don't do this in a normal reducer!
state.status = 'loading'
return state
}
default:
return state
}
}

糟糕。我们的整个应用程序崩溃了!发生了什么事?

Immutability check middleware error

此错误消息是一件好事 - 我们在应用程序中捕获了一个错误!configureStore 特别添加了一个额外的中间件,每当它看到意外修改我们的状态时(仅在开发模式下),它会自动抛出错误。这有助于捕获我们在编写代码时可能犯的错误。

包清理

Redux Toolkit 已经包含了我们正在使用的几个包,比如 reduxredux-thunkreselect,并重新导出了这些 API。因此,我们可以清理一下我们的项目。

首先,我们可以将 createSelector 的导入改为从 '@reduxjs/toolkit' 而不是 'reselect'。然后,我们可以删除 package.json 中列出的单独的包。

npm uninstall redux redux-thunk reselect

需要明确的是,我们仍然在使用这些包,并且需要安装它们。但是,由于 Redux Toolkit 依赖于它们,因此当我们安装 @reduxjs/toolkit 时,它们将自动安装,所以我们不需要在 package.json 文件中专门列出其他包。

编写切片

随着我们在应用程序中添加了新功能,切片文件变得越来越大,也越来越复杂。特别是,todosReducer 由于所有用于不可变更新的嵌套对象扩展而变得难以阅读,并且我们编写了多个操作创建器函数。

Redux Toolkit 有一个 createSlice API,它可以帮助我们简化 Redux reducer 逻辑和操作createSlice 为我们做了几件重要的事情

  • 我们可以将 case reducer 函数编写为对象内部的函数,而不是必须编写 switch/case 语句。
  • reducer 将能够编写更短的不可变更新逻辑。
  • 所有操作创建器将根据我们提供的 reducer 函数自动生成。

使用 createSlice

createSlice 接受一个包含三个主要选项字段的对象。

  • name:一个字符串,将用作生成的操作类型的前缀。
  • initialState:reducer 的初始状态。
  • reducers:一个对象,其中键是字符串,值是“case reducer”函数,这些函数将处理特定操作。

让我们先看一个小的独立示例。

createSlice 示例
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
entities: [],
status: null
}

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
// ✅ This "mutating" code is okay inside of createSlice!
state.entities.push(action.payload)
},
todoToggled(state, action) {
const todo = state.entities.find(todo => todo.id === action.payload)
todo.completed = !todo.completed
},
todosLoading(state, action) {
return {
...state,
status: 'loading'
}
}
}
})

export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions

export default todosSlice.reducer

在这个示例中,有几件事需要注意。

  • 我们在 reducers 对象中编写 case reducer 函数,并为它们提供可读的名称。
  • createSlice 将自动生成与我们提供的每个 case reducer 函数相对应的操作创建器
  • createSlice 在默认情况下会自动返回现有状态。
  • createSlice 允许我们安全地“修改”我们的状态!
  • 但是,如果我们想的话,也可以像以前一样创建不可变的副本。

生成的 action creators 将作为 slice.actions.todoAdded 可用,我们通常像之前编写 action creators 一样,将它们解构并单独导出。完整的 reducer 函数作为 slice.reducer 可用,我们通常 export default slice.reducer,这与之前相同。

那么这些自动生成的 action 对象是什么样的呢?让我们尝试调用其中一个并记录 action 来看看。

console.log(todoToggled(42))
// {type: 'todos/todoToggled', payload: 42}

createSlice 为我们生成了 action 类型字符串,方法是将 slice 的 name 字段与我们编写的 reducer 函数的 todoToggled 名称组合起来。默认情况下,action creator 接受一个参数,它将该参数放入 action 对象中作为 action.payload

在生成的 reducer 函数内部,createSlice 将检查调度 action 的 action.type 是否与它生成的名称之一匹配。如果是,它将运行该 case reducer 函数。这与我们使用 switch/case 语句自己编写的模式完全相同,但 createSlice 会自动为我们完成。

还值得详细讨论一下“修改”方面。

使用 Immer 进行不可变更新

之前,我们讨论了“修改”(修改现有对象/数组值)和“不可变性”(将值视为不可更改的东西)。

危险

在 Redux 中,我们的 reducer 永远 不允许修改原始/当前状态值!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

那么,如果我们不能更改原始值,我们如何返回更新后的状态呢?

提示

Reducer 只能创建原始值的副本,然后它们可以修改副本。

// This is safe, because we made a copy
return {
...state,
value: 123
}

正如您在本教程中所见,我们可以通过使用 JavaScript 的数组/对象展开运算符和其他返回原始值副本的函数来手动编写不可变更新。但是,手动编写不可变更新逻辑难,并且在 reducer 中意外修改状态是 Redux 用户最常见的错误。

这就是 Redux Toolkit 的 createSlice 函数让您以更轻松的方式编写不可变更新的原因!

createSlice 在内部使用了一个名为 Immer 的库。Immer 使用一个名为 Proxy 的特殊 JS 工具来包装你提供的数据,并让你编写“修改”这些包装数据的代码。但是,**Immer 会跟踪你尝试进行的所有更改,然后使用这些更改列表返回一个安全且不可变的更新值**,就好像你手动编写了所有不可变的更新逻辑一样。

所以,与其这样写

function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

你可以编写看起来像这样的代码

function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}

这更容易阅读!

但是,这里有一件非常重要的事情要记住

危险

只能在 Redux Toolkit 的 createSlicecreateReducer 中编写“修改”逻辑,因为它们在内部使用了 Immer!如果你在没有 Immer 的情况下在 reducer 中编写修改逻辑,它修改状态并导致错误!

Immer 仍然允许我们手动编写不可变的更新并自己返回新值,如果我们想这样做的话。你甚至可以混合使用。例如,从数组中删除一个项目通常更容易使用 array.filter() 来完成,所以你可以调用它,然后将结果分配给 state 来“修改”它

// can mix "mutating" and "immutable" code inside of Immer:
state.todos = state.todos.filter(todo => todo.id !== action.payload)

转换 Todos Reducer

让我们开始将我们的 todos 切片文件转换为使用 createSlice。我们首先从我们的 switch 语句中选择几个特定的案例来展示这个过程是如何工作的。

src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
status: 'idle',
entities: {}
}

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
}
}
})

export const { todoAdded, todoToggled } = todosSlice.actions

export default todosSlice.reducer

我们示例应用程序中的 todos reducer 仍然使用嵌套在父对象中的规范化状态,因此这里的代码与我们刚刚看到的微型 createSlice 示例略有不同。还记得我们之前是如何编写了许多嵌套的展开运算符来切换那个 todo 的吗?现在,相同的代码更短,更容易阅读。

让我们在这个 reducer 中添加几个案例。

src/features/todos/todosSlice.js
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted(state, action) {
delete state.entities[action.payload]
}
}
})

export const { todoAdded, todoToggled, todoColorSelected, todoDeleted } =
todosSlice.actions

export default todosSlice.reducer

todoAddedtodoToggled 的 action creator 只需要接受一个参数,比如一个完整的 todo 对象或一个 todo ID。但是,如果我们需要传入多个参数,或者执行我们之前讨论过的一些“准备”逻辑,比如生成一个唯一的 ID 呢?

createSlice 允许我们通过向 reducer 添加一个“准备回调”来处理这些情况。我们可以传递一个包含名为 reducerprepare 的函数的对象。当我们调用生成的 action creator 时,prepare 函数将使用传入的任何参数调用。然后它应该创建一个并返回一个包含 payload 字段(或者,可选地,metaerror 字段)的对象,与Flux Standard Action 约定匹配。

在这里,我们使用了一个准备回调来让我们的 todoColorSelected action creator 接受单独的 todoIdcolor 参数,并将它们放在 action.payload 中作为一个对象。

同时,在 todoDeleted reducer 中,我们可以使用 JS delete 运算符从我们的规范化状态中删除项目。

我们可以使用相同的模式来重写 todosSlice.jsfiltersSlice.js 中的其余 reducer。

以下是将所有切片转换后的代码。

编写 Thunk

我们已经了解了如何 编写分发 "加载"、"请求成功" 和 "请求失败" 操作的 thunk。我们必须编写操作创建者、操作类型以及处理这些情况的 reducer。

由于这种模式非常常见,Redux Toolkit 提供了一个 createAsyncThunk API,可以为我们生成这些 thunk。它还会为这些不同的请求状态操作生成操作类型和操作创建者,并根据生成的 Promise 自动分发这些操作。

提示

Redux Toolkit 有一个新的 RTK Query 数据获取 API。RTK Query 是一个专门为 Redux 应用程序构建的数据获取和缓存解决方案,可以消除编写任何 thunk 或 reducer 来管理数据获取的需要。我们鼓励您尝试一下,看看它是否可以帮助简化您自己的应用程序中的数据获取代码!

我们很快就会更新 Redux 教程,其中将包含有关使用 RTK Query 的部分。在此之前,请参阅 Redux Toolkit 文档中的 RTK Query 部分

使用 createAsyncThunk

让我们用 createAsyncThunk 生成一个 thunk 来替换我们的 fetchTodos thunk。

createAsyncThunk 接受两个参数

  • 一个将用作生成的 action 类型的前缀的字符串
  • 一个 "payload 创建者" 回调函数,它应该返回一个 Promise。这通常使用 async/await 语法编写,因为 async 函数会自动返回一个 promise。
src/features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// omit imports and state

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit reducer cases
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
}
})

// omit exports

我们将 'todos/fetchTodos' 作为字符串前缀传递,以及一个 "payload 创建者" 函数,该函数调用我们的 API 并返回一个包含获取数据的 promise。在内部,createAsyncThunk 将生成三个操作创建者和操作类型,以及一个 thunk 函数,该函数在调用时会自动分发这些操作。在本例中,操作创建者及其类型为

  • fetchTodos.pending: todos/fetchTodos/pending
  • fetchTodos.fulfilled: todos/fetchTodos/fulfilled
  • fetchTodos.rejected: todos/fetchTodos/rejected

然而,这些 action creators 和类型是在 createSlice 调用之外定义的。我们无法在 createSlice.reducers 字段中处理它们,因为它们也会生成新的 action 类型。我们需要一种方法让我们的 createSlice 调用监听其他在其他地方定义的 action 类型。

createSlice 还接受一个 extraReducers 选项,我们可以让同一个 slice reducer 监听其他 action 类型。此字段应该是一个带有 builder 参数的回调函数,我们可以调用 builder.addCase(actionCreator, caseReducer) 来监听其他 action。

因此,这里我们调用了 builder.addCase(fetchTodos.pending, caseReducer)。当该 action 被分派时,我们将运行将 state.status 设置为 'loading' 的 reducer,与我们之前在 switch 语句中编写该逻辑时相同。我们可以对 fetchTodos.fulfilled 做同样的事情,并处理我们从 API 收到的数据。

再举一个例子,让我们转换 saveNewTodo。这个 thunk 以新 todo 对象的 text 作为参数,并将其保存到服务器。我们如何处理它?

src/features/todos/todosSlice.js
// omit imports

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit case reducers
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, (state, action) => {
const todo = action.payload
state.entities[todo.id] = todo
})
}
})

// omit exports and selectors

saveNewTodo 的过程与我们对 fetchTodos 所做的相同。我们调用 createAsyncThunk,并传入 action 前缀和一个 payload 创建者。在 payload 创建者内部,我们进行异步 API 调用,并返回一个结果值。

在这种情况下,当我们调用 dispatch(saveNewTodo(text)) 时,text 值将作为第一个参数传递给 payload 创建者。

虽然我们在这里不会详细介绍 createAsyncThunk,但以下是一些其他快速说明以供参考

  • 您只能在分派 thunk 时传递一个参数。如果您需要传递多个值,请将它们放在一个对象中传递
  • payload 创建者将接收一个对象作为其第二个参数,其中包含 {getState, dispatch} 和一些其他有用的值
  • thunk 在运行您的 payload 创建者之前分派 pending action,然后根据您返回的 Promise 成功还是失败分派 fulfilledrejected

规范化状态

我们之前已经了解了如何通过将项目存储在以项目 ID 为键的对象中来“规范化”状态。这使我们能够通过 ID 查找任何项目,而无需遍历整个数组。但是,手动编写更新规范化状态的逻辑既冗长又乏味。使用 Immer 编写“可变”更新代码可以简化操作,但仍然可能存在大量重复 - 我们可能在应用程序中加载许多不同类型的项目,并且每次都需要重复相同的 reducer 逻辑。

Redux Toolkit 包含一个 createEntityAdapter API,它为使用规范化状态的典型数据更新操作提供了预先构建的 reducer。这包括向切片添加、更新和删除项目。createEntityAdapter 还生成一些记忆化的选择器,用于从存储中读取值

使用 createEntityAdapter

让我们用 createEntityAdapter 替换我们规范化的实体 reducer 逻辑。

调用 createEntityAdapter 会给我们一个“适配器”对象,其中包含几个预先制作的 reducer 函数,包括

  • addOne / addMany: 将新项目添加到状态
  • upsertOne / upsertMany: 添加新项目或更新现有项目
  • updateOne / updateMany: 通过提供部分值来更新现有项目
  • removeOne / removeMany: 根据 ID 删除项目
  • setAll: 替换所有现有项目

我们可以将这些函数用作 case reducer,或者用作 createSlice 中的“可变助手”。

适配器还包含

  • getInitialState: 返回一个看起来像 { ids: [], entities: {} } 的对象,用于存储项目的规范化状态以及所有项目 ID 的数组
  • getSelectors: 生成一组标准的选择器函数

让我们看看如何在 todos 切片中使用它们

src/features/todos/todosSlice.js
import {
createSlice,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
// omit some imports

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
status: 'idle'
})

// omit thunks

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit some reducers
// Use an adapter reducer function to remove a todo by ID
todoDeleted: todosAdapter.removeOne,
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
// Use an adapter function as a "mutating" update helper
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
// Use another adapter function as a reducer to add a todo
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})

// omit selectors

不同的适配器 reducer 函数根据函数的不同而接受不同的值,所有这些值都在 action.payload 中。“添加”和“更新”函数接受单个项目或项目数组,“删除”函数接受单个 ID 或 ID 数组,等等。

getInitialState 允许我们传入将被包含的额外状态字段。在本例中,我们传入了一个 status 字段,这给了我们一个最终的 todos 切片状态 {ids, entities, status},就像我们之前一样。

我们还可以替换一些 todos 选择器函数。getSelectors 适配器函数将生成像 selectAll 这样的选择器,它返回所有项目的数组,以及 selectById,它返回一个项目。但是,由于 getSelectors 不知道我们的数据在整个 Redux 状态树中的位置,因此我们需要传入一个小的选择器,它从整个状态树中返回这个切片。让我们切换到使用它们。由于这是我们代码的最后一个重大更改,我们将这次包含整个 todos 切片文件,以查看使用 Redux Toolkit 的代码最终版本是什么样子

src/features/todos/todosSlice.js
import {
createSlice,
createSelector,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
import { client } from '../../api/client'
import { StatusFilters } from '../filters/filtersSlice'

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
status: 'idle'
})

// Thunk functions
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted: todosAdapter.removeOne,
allTodosCompleted(state, action) {
Object.values(state.entities).forEach(todo => {
todo.completed = true
})
},
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})

export const {
allTodosCompleted,
completedTodosCleared,
todoAdded,
todoColorSelected,
todoDeleted,
todoToggled
} = todosSlice.actions

export default todosSlice.reducer

export const { selectAll: selectTodos, selectById: selectTodoById } =
todosAdapter.getSelectors(state => state.todos)

export const selectTodoIds = createSelector(
// First, pass one or more "input selector" functions:
selectTodos,
// Then, an "output selector" that receives all the input results as arguments
// and returns a final result value
todos => todos.map(todo => todo.id)
)

export const selectFilteredTodos = createSelector(
// First input selector: all todos
selectTodos,
// Second input selector: all filter values
state => state.filters,
// 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
})
}
)

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)
)

我们调用了 todosAdapter.getSelectors,并传入了一个 state => state.todos 选择器,它返回了这部分状态。从那里,适配器生成一个 selectAll 选择器,它像往常一样接受整个 Redux 状态树,并循环遍历 state.todos.entitiesstate.todos.ids,为我们提供完整的待办事项对象数组。由于 selectAll 没有告诉我们选择什么,我们可以使用解构语法将函数重命名为 selectTodos。类似地,我们可以将 selectById 重命名为 selectTodoById

请注意,我们其他的选择器仍然使用 selectTodos 作为输入。这是因为无论我们是否将数组保留为整个 state.todos,将其保留为嵌套数组,还是将其存储为规范化的对象并转换为数组,它始终返回一个待办事项对象数组。即使我们对数据存储方式进行了所有这些更改,选择器的使用使我们能够保持其余代码不变,而记忆选择器的使用通过避免不必要的重新渲染帮助 UI 更好地执行。

你学到了什么

恭喜!你已经完成了“Redux 基础”教程!

你现在应该对 Redux 是什么、它是如何工作的以及如何正确使用它有了深刻的理解

  • 管理全局应用程序状态
  • 将应用程序的状态保留为纯 JS 数据
  • 编写描述应用程序中“发生了什么”的动作对象
  • 使用 reducer 函数,它查看当前状态和一个动作,并不可变地创建一个新的状态作为响应
  • 使用 useSelector 在 React 组件中读取 Redux 状态
  • 使用 useDispatch 从 React 组件中分派动作

此外,你已经了解了 Redux Toolkit 如何简化 Redux 逻辑的编写,以及为什么 Redux Toolkit 是编写真实 Redux 应用程序的标准方法。通过首先了解如何“手动”编写 Redux 代码,应该清楚 Redux Toolkit API(如 createSlice)为你做了什么,这样你就不用自己编写这些代码了。

信息

有关 Redux Toolkit 的更多信息,包括使用指南和 API 参考,请参阅

让我们最后再看一遍完成的待办事项应用程序,包括所有已转换为使用 Redux Toolkit 的代码

我们还将对本节中学习的关键要点进行最后总结

总结
  • Redux Toolkit (RTK) 是编写 Redux 逻辑的标准方式
    • RTK 包含简化大多数 Redux 代码的 API
    • RTK 包裹在 Redux 核心周围,并包含其他有用的包
  • configureStore 使用良好的默认设置设置 Redux 存储
    • 自动组合切片 reducer 以创建根 reducer
    • 自动设置 Redux DevTools 扩展和调试中间件
  • createSlice 简化了编写 Redux 操作和 reducer 的过程
    • 根据切片/reducer 名称自动生成操作创建者
    • reducer 可以使用 Immer 在 createSlice 中“修改”状态
  • createAsyncThunk 为异步调用生成 thunk
    • 自动生成 thunk + pending/fulfilled/rejected 操作创建者
    • 调度 thunk 会运行你的有效负载创建者并调度操作
    • Thunk 操作可以在 createSlice.extraReducers 中处理
  • createEntityAdapter 为规范化状态提供 reducer + 选择器
    • 包括用于添加/更新/删除项目的常见任务的 reducer 函数
    • selectAllselectById 生成记忆选择器

学习和使用 Redux 的下一步

现在您已经完成了本教程,我们有一些建议,您可以尝试下一步来了解更多关于 Redux 的知识。

本“基础知识”教程侧重于 Redux 的低级方面:手动编写操作类型和不可变更新,Redux 存储和中间件的工作原理,以及为什么我们使用操作创建者和规范化状态等模式。此外,我们的待办事项示例应用程序相当小,并非旨在作为构建完整应用程序的现实示例。

然而,我们的 “Redux Essentials” 教程 专注于教你如何构建“现实世界”类型的应用程序。它重点介绍如何使用 Redux Toolkit “正确使用 Redux”,并讨论在大型应用程序中会看到的更现实的模式。它涵盖了与本“基础知识”教程相同的许多主题,例如为什么 reducer 需要使用不可变更新,但重点在于构建一个真正可用的应用程序。我们强烈建议您将“Redux Essentials”教程作为下一步。

同时,在本教程中涵盖的概念应该足以让你开始使用 React 和 Redux 构建自己的应用程序。现在是尝试自己进行项目的好时机,以巩固这些概念并了解它们在实践中的工作方式。如果你不确定要构建什么类型的项目,请查看 这个应用程序项目创意列表 以获得一些灵感。

使用 Redux 部分中,包含了一些重要概念的信息,例如 如何构建你的 reducer,以及 我们的风格指南页面 中包含了关于我们推荐的模式和最佳实践的重要信息。

如果你想了解更多关于为什么存在 Redux,它试图解决什么问题,以及它应该如何使用,请查看 Redux 维护者 Mark Erikson 关于 Redux 的道,第一部分:实现和意图Redux 的道,第二部分:实践和哲学 的文章。

如果你需要关于 Redux 的问题帮助,请加入 Discord 上 Reactiflux 服务器的 #redux 频道

感谢您阅读本教程,我们希望您享受使用 Redux 构建应用程序的乐趣!