Redux 基础教程,第 8 部分:使用 Redux Toolkit 的现代 Redux
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
- 如何使用 Redux Toolkit 简化 Redux 逻辑
- 学习和使用 Redux 的后续步骤
恭喜你完成了本教程的最后部分!在结束前我们还有一个重要主题要探讨。
如需回顾之前内容,请参考以下摘要:
Recap: What You've Learned
- Part 1: Overview:
- what Redux is, when/why to use it, and the basic pieces of a Redux app
- Part 2: Concepts and Data Flow:
- How Redux uses a "one-way data flow" pattern
- Part 3: State, Actions, and Reducers:
- Redux state is made of plain JS data
- Actions are objects that describe "what happened" events in an app
- Reducers take current state and an action, and calculate a new state
- Reducers must follow rules like "immutable updates" and "no side effects"
- Part 4: Store:
- The
createStoreAPI creates a Redux store with a root reducer function - Stores can be customized using "enhancers" and "middleware"
- The Redux DevTools extension lets you see how your state changes over time
- The
- Part 5: UI and React:
- Redux is separate from any UI, but frequently used with React
- React-Redux provides APIs to let React components talk to Redux stores
useSelectorreads values from Redux state and subscribes to updatesuseDispatchlets components dispatch actions<Provider>wraps your app and lets components access the store
- Part 6: Async Logic and Data Fetching:
- Redux middleware allow writing logic that has side effects
- Middleware add an extra step to the Redux data flow, enabling async logic
- Redux "thunk" functions are the standard way to write basic async logic
- Part 7: Standard Redux Patterns:
- Action creators encapsulate preparing action objects and thunks
- Memoized selectors optimize calculating transformed data
- Request status should be tracked with loading state enum values
- Normalized state makes it easier to look up items by IDs
如你所见,Redux 的许多实现需要编写冗长代码,例如不可变更新、动作类型和创建器、状态规范化等。这些模式虽有其必要性,但手动编写颇为繁琐。此外,Redux 存储的初始化流程包含多个步骤,我们还需自行实现诸如 thunk 中派发"加载中"动作或处理规范化数据等逻辑。最重要的是,开发者常不确定何为 Redux 逻辑的"正确写法"。
为此 Redux 团队推出了 Redux Toolkit:官方推荐的、开箱即用的高效 Redux 开发工具集。
Redux Toolkit 包含构建 Redux 应用的核心工具包和函数。它内置了最佳实践方案,简化了大多数 Redux 任务,规避了常见错误,让 Redux 应用开发更加高效。
因此,Redux Toolkit 已成为编写 Redux 应用逻辑的标准方式。本教程中你手动编写的 Redux 逻辑确实能正常运行,但实际开发中不应手动编写——我们讲解这些方法是为了让你理解 Redux 的运作原理。不过在真实项目中,你应使用 Redux Toolkit 来编写 Redux 逻辑。
当你使用 Redux Toolkit 时,我们之前介绍的所有概念(actions、reducers、store 设置、action creators、thunks 等)仍然存在,但 Redux Toolkit 提供了更简洁的代码实现方式。
Redux Toolkit 仅 处理 Redux 逻辑 —— 我们仍需使用 React-Redux 让 React 组件与 Redux store 交互,包括 useSelector 和 useDispatch。
接下来我们将演示如何使用 Redux Toolkit 简化待办事项应用示例中的现有代码。我们将主要重写 "slice" 文件,同时保持所有 UI 代码不变。
在继续之前,请将 Redux Toolkit 包添加到你的应用:
npm install @reduxjs/toolkit
Store 设置
我们已经经历了 Redux store 设置逻辑的多次迭代。当前版本如下:
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
import { createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
const composedEnhancer = composeWithDevTools(applyMiddleware(thunk))
const store = createStore(rootReducer, composedEnhancer)
export default store
注意该设置流程包含多个步骤。我们需要:
-
将 slice reducers 组合成根 reducer
-
将根 reducer 导入 store 文件
-
导入 thunk 中间件、
applyMiddleware和composeWithDevToolsAPI -
使用中间件和开发工具创建 store enhancer
-
使用根 reducer 创建 store
若能减少这些步骤就太好了。
使用 configureStore
Redux Toolkit 提供了简化 store 设置流程的 configureStore API。configureStore 封装了 Redux 核心的 createStore API,并自动处理大部分 store 设置工作。实际上我们可以将其简化为一步:
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 调用完成了所有工作:
-
将
todosReducer和filtersReducer组合成根 reducer 函数,处理形如{todos, filters}的根状态 -
使用该根 reducer 创建 Redux store
-
自动添加
thunk中间件 -
自动添加额外中间件检查常见错误(如意外变更状态)
-
自动配置 Redux DevTools 扩展连接
我们可以打开待办事项应用示例来验证其是否正常工作。现有功能代码一切正常!由于我们正在分发 actions、分发 thunks、在 UI 中读取状态以及在 DevTools 中查看 action 历史记录,所有这些部分都必须正常运行。我们所做的仅仅是替换了 store 设置代码。
现在让我们测试意外变更状态的情况。如果修改 "todos loading" reducer 使其直接更改状态字段(而非不可变地创建副本)会怎样?
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
}
}
糟糕!整个应用崩溃了!发生了什么?

这个错误信息是件好事 —— 我们捕获了应用中的一个 bug! configureStore 特意添加了额外中间件,当检测到状态被意外变更时(仅在开发模式下)自动抛出错误。这有助于捕获我们在编写代码时可能犯的错误。
包清理
Redux Toolkit 已包含我们正在使用的多个包(如 redux、redux-thunk 和 reselect),并重新导出这些 API。因此我们可以简化项目依赖。
首先将 createSelector 的导入来源从 'reselect' 改为 '@reduxjs/toolkit'。然后移除 package.json 中列出的独立包:
npm uninstall redux redux-thunk reselect
需要明确的是,我们仍然在使用这些包且需要安装它们。但由于 Redux Toolkit 依赖这些包,安装 @reduxjs/toolkit 时会自动包含它们,因此无需在 package.json 中单独列出。
编写切片(Slices)
随着功能增加,我们的切片文件变得越发庞大复杂。特别是 todosReducer,由于嵌套的对象展开不可变更新导致可读性下降,且我们编写了多个 action creator 函数。
Redux Toolkit 的 createSlice API 可简化 Redux reducer 逻辑和 action。createSlice 为我们提供了多项重要功能:
-
可将 case reducer 写成对象内的函数,无需编写
switch/case语句 -
reducer 能使用更简洁的不可变更新逻辑
-
所有 action creator 会根据提供的 reducer 函数自动生成
使用 createSlice
createSlice 接收包含三个主选项字段的对象:
-
name:用作生成 action 类型前缀的字符串 -
initialState:reducer 的初始状态 -
reducers:键为字符串、值为处理特定 action 的 "case reducer" 函数的对象
先看一个独立小示例。
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 对应的 action creator -
默认情况下
createSlice会直接返回现有状态 -
createSlice允许安全地"直接变更"状态! -
但也可按传统方式创建不可变副本
生成的 action creator 可通过 slice.actions.todoAdded 访问,我们通常会像之前那样单独解构导出。完整的 reducer 函数通过 slice.reducer 提供,同样按惯例 export default slice.reducer。
这些自动生成的 action 对象是什么结构?我们调用并记录日志查看:
console.log(todoToggled(42))
// {type: 'todos/todoToggled', payload: 42}
createSlice 通过组合切片的 name 字段与 reducer 函数名(如 todoToggled)生成 action 类型字符串。默认情况下 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 的 createSlice 和 createReducer 中编写“变更”逻辑,因为它们内部使用了 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 语句中挑选几个特定案例来演示转换过程。
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 添加更多案例。
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
todoAdded 和 todoToggled 的动作创建器只需接受单个参数(如完整 todo 对象或 todo ID)。但如果需要传入多个参数,或执行生成唯一 ID 等“预处理”逻辑该怎么办?
createSlice 允许我们通过向 reducer 添加“准备回调”来处理这种情况。我们可以传递包含 reducer 和 prepare 函数的对象。调用生成的动作创建器时,prepare 函数会接收传入的所有参数,然后创建并返回包含 payload 字段的对象(或可选的 meta/error 字段),这符合 Flux 标准动作规范。
这里我们使用准备回调让 todoColorSelected 动作创建器接受独立的 todoId 和 color 参数,并将它们组合为 action.payload 中的对象。
同时在 todoDeleted reducer 中,我们可以使用 JS 的 delete 运算符从规范化状态中移除项。
我们可以用相同模式重写 todosSlice.js 和 filtersSlice.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 接受两个参数:
-
一个字符串,用作生成的动作类型的前缀
-
"有效载荷创建器"回调函数:需返回 Promise 对象。通常使用
async/await语法编写,因为async函数会自动返回 Promise。
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' 作为字符串前缀,以及调用 API 并返回包含获取数据的 Promise 的"有效载荷创建器"函数。在内部,createAsyncThunk 会生成三个动作创建器及其类型,外加一个在调用时自动派发这些动作的 thunk 函数。本例生成的动作创建器及其类型为:
-
fetchTodos.pending:todos/fetchTodos/pending -
fetchTodos.fulfilled:todos/fetchTodos/fulfilled -
fetchTodos.rejected:todos/fetchTodos/rejected
但这些动作创建器和类型是在 createSlice 调用_外部_定义的。我们无法在 createSlice.reducers 字段中处理它们,因为该字段也会生成新的动作类型。需要让 createSlice 能监听到_外部定义_的其他动作类型。
createSlice 还接受 extraReducers 选项,允许同一个切片 reducer 监听其他动作类型。该选项应是一个接收 builder 参数的回调函数,可通过 builder.addCase(actionCreator, caseReducer) 监听其他动作。
因此,这里我们调用 builder.addCase(fetchTodos.pending, caseReducer)。当该动作被派发时,将执行设置 state.status = 'loading' 的 reducer,效果与之前在 switch 语句中编写的逻辑相同。同样可处理 fetchTodos.fulfilled 并响应 API 返回的数据。
再以转换 saveNewTodo 为例。该 thunk 接收新待办事项的 text 参数并保存至服务器。该如何实现?
// 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,传入动作前缀和有效载荷创建器。在有效载荷创建器中发起异步 API 调用并返回结果值。
此时调用 dispatch(saveNewTodo(text)) 时,text 值将作为第一个参数传递给有效载荷创建器。
本文不深入探讨 createAsyncThunk,以下补充要点供参考:
-
派发 thunk 时只能传递一个参数。需传递多个值时,请使用对象封装
-
有效载荷创建器的第二个参数是包含
{getState, dispatch}等实用方法的对象 -
thunk 在执行有效载荷创建器前派发
pending动作,随后根据返回 Promise 的成功/失败状态派发fulfilled或rejected
规范化状态管理
此前我们了解了如何通过按条目 ID 作为键来存储对象以实现状态“规范化”。这样我们可以直接通过 ID 查找任意条目,而无需遍历整个数组。然而,手动编写更新规范化状态的逻辑冗长且繁琐。使用 Immer 编写“直接变更”的更新代码虽然简化了操作,但重复性依然很高——我们可能在应用中加载多种不同类型的条目,却每次都要重复相同的 reducer 逻辑。
Redux Toolkit 提供 createEntityAdapter API,其中包含针对规范化状态的典型数据更新操作预构建的 reducer。这包括在切片中添加、更新和移除条目。createEntityAdapter 还会生成一些记忆化 selector,用于从 store 中读取值。
使用 createEntityAdapter
现在让我们用 createEntityAdapter 替换之前的规范化实体 reducer 逻辑。
调用 createEntityAdapter 会返回一个“适配器”对象,其中包含多个预制的 reducer 函数,例如:
-
addOne/addMany:向状态中添加新条目 -
upsertOne/upsertMany:添加新条目或更新现有条目 -
updateOne/updateMany:通过提供部分值更新现有条目 -
removeOne/removeMany:根据 ID 移除条目 -
setAll:替换所有现有条目
这些函数既可用作 case reducer,也可在 createSlice 中作为“变更辅助函数”使用。
适配器还包含:
-
getInitialState:返回一个形如{ ids: [], entities: {} }的对象,用于存储条目的规范化状态及所有条目的 ID 数组 -
getSelectors:生成一组标准 selector 函数
接下来看看如何在我们待办事项的切片中使用这些功能:
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 字段,最终得到待办事项切片状态 {ids, entities, status},与之前的结构类似。
我们也可以替换部分待办事项的 selector 函数。适配器的 getSelectors 函数会生成如 selectAll(返回所有条目的数组)和 selectById(返回单个条目)等 selector。但由于 getSelectors 无法自动获知数据在完整 Redux 状态树中的位置,我们需要传入一个小型 selector 来从完整状态树中提取当前切片。现在我们就改用这些 selector。由于这是代码的最后一次重大调整,这次我们将展示整个待办事项切片文件,以便查看使用 Redux Toolkit 的最终代码形态:
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 这个 selector 来获取状态中的对应切片。接着适配器会生成 selectAll selector,它接收_完整_ Redux 状态树(与常规用法一致),遍历 state.todos.entities 和 state.todos.ids 后返回完整的待办事项对象数组。由于 selectAll 未明确表明选择内容,我们可以使用解构语法将其重命名为 selectTodos。同理,可将 selectById 重命名为 selectTodoById。
请注意,其他选择器仍在使用 selectTodos 作为输入。这是因为无论我们将待办事项数组作为完整的 state.todos、嵌套数组存储,还是存储为规范化对象再转换为数组,它始终返回待办事项对象数组。即使我们不断改变数据存储方式,选择器的使用仍能保持代码其余部分不变,而记忆化选择器通过避免不必要的重新渲染提升了 UI 性能。
学习要点
恭喜!你已完成 "Redux 基础" 教程!
现在,你应该对以下内容有了扎实的理解:
-
管理全局应用状态
-
将应用状态保持为纯 JS 数据
-
编写描述应用中"发生了什么"的动作对象
-
使用 reducer 函数:根据当前状态和动作,以不可变方式创建新状态
-
在 React 组件中使用
useSelector读取 Redux 状态 -
在 React 组件中使用
useDispatch分发动作
此外,你已经了解 Redux Toolkit 如何简化 Redux 逻辑编写,以及为何 Redux Toolkit 是开发实际 Redux 应用的标准方式。通过先学习"手动"编写 Redux 代码,你会更清楚 createSlice 等 API 的自动化价值,从而无需再手动编写这些代码。
有关 Redux Toolkit 的更多信息(包括使用指南和 API 参考),请访问:
- Redux Toolkit 文档:https://redux-toolkit.js.org
让我们最后再看一眼完整的待办事项应用(包含所有已转换为 Redux Toolkit 的代码):
最后,我们将回顾你在本节中学到的关键点:
- Redux Toolkit (RTK) 是编写 Redux 逻辑的标准方式
- RTK 包含可简化大多数 Redux 代码的 API
- RTK 封装了 Redux 核心,并包含其他实用包
configureStore以智能默认值配置 Redux store- 自动组合切片 reducer 生成根 reducer
- 自动集成 Redux DevTools 扩展和调试中间件
createSlice简化 Redux 动作和 reducer 编写- 根据切片/reducer 名称自动生成动作创建器
- Reducer 可在
createSlice中使用 Immer 进行"直接变更"式状态更新
createAsyncThunk为异步调用生成 thunk- 自动生成 thunk 及
pending/fulfilled/rejected动作创建器 - 分发 thunk 会执行有效负载创建器并分发相应动作
- thunk 动作可在
createSlice.extraReducers中处理
- 自动生成 thunk 及
createEntityAdapter为规范化状态提供 reducer + 选择器- 包含添加/更新/删除等常见任务的 reducer 函数
- 为
selectAll和selectById生成记忆化选择器
学习和使用 Redux 的后续步骤
完成本教程后,以下是深入探索 Redux 的后续建议:
本"基础"教程聚焦 Redux 底层细节:手动编写动作类型和不可变更新、理解 Redux store 和中间件工作原理,以及动作创建器和规范化状态等模式的价值。此外,我们的待办事项示例应用规模较小,并非完整应用的实际范例。
不过,我们的《Redux Essentials》教程专门教你如何构建“真实世界”型应用。它聚焦于使用 Redux Toolkit 的“正确使用 Redux 方式”,并讨论在大型应用中更常见的实际模式。虽然它涵盖了许多与本“基础”教程相同的主题(例如 reducer 为何需要使用不可变更新),但重点在于构建实际可运行的应用。我们强烈建议你接下来阅读《Redux Essentials》教程。
同时,本教程涵盖的概念应足以让你开始使用 React 和 Redux 构建自己的应用。现在是亲自实践项目的绝佳时机——既能巩固这些概念,又能观察它们在实际中如何运作。如果不确定构建什么类型的项目,可参考这个应用创意列表获取灵感。
使用 Redux 章节包含多个重要概念的信息,例如如何组织 reducer,而我们的风格指南则提供了关于推荐模式和最佳实践的重要参考。
如果您想深入了解 Redux 的_存在意义_、它试图解决的问题以及设计使用哲学,请参阅 Redux 维护者 Mark Erikson 的博文:Redux 之道 第一部分:实现与意图和Redux 之道 第二部分:实践与哲学。
如需获取 Redux 问题帮助,欢迎加入 Discord 上 Reactiflux 服务器的 #redux 频道。
感谢您阅读本教程,祝您在使用 Redux 构建应用的过程中收获满满!