使用指南
Redux 核心库故意保持无偏见。它让你决定如何处理所有事情,比如存储设置,你的状态包含什么,以及你如何构建你的 reducers。
在某些情况下,这是好的,因为它给你灵活性,但这种灵活性并不总是需要的。有时我们只想以最简单的方式开始,有一些好的默认行为。或者,也许你正在编写一个更大的应用程序,并发现自己正在编写一些相似的代码,你希望减少你必须手动编写的代码量。
如快速开始页面所述,Redux Toolkit 的目标是帮助简化常见的 Redux 使用案例。它并不打算成为你可能想要用 Redux 做的所有事情的完整解决方案,但它应该使你需要编写的大部分 Redux 相关代码更简单(或在某些情况下,消除一些手写代码)。
Redux Toolkit 导出了几个你可以在你的应用程序中使用的单独函数,并添加了一些常与 Redux 一起使用的其他包的依赖(如 Reselect 和 Redux-Thunk)。这让你决定如何在你自己的应用程序中使用这些,无论它是一个全新的项目还是更新一个大型现有的应用。
让我们看看 Redux Toolkit 如何帮助你 改进你的 Redux 相关代码。
存储设置
每个 Redux 应用都需要配置和创建一个 Redux 存储。这通常涉及几个步骤:
- 导入或创建根 reducer 函数
- 设置中间件,可能至少包括一个处理异步逻辑的中间件
- 配置 Redux DevTools 扩展
- 可能根据应用程序是否为开发或生产构建来改变一些逻辑
手动存储设置
以下示例来自 Redux 文档的 配置你的存储 页面,显示了一个典型的存储设置过程:
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
这个例子是可读的,但过程并不总是直接的:
- 基本 的 Redux
createStore
函数接受位置参数:(rootReducer, preloadedState, enhancer)
。有时候很容易忘记哪个参数是哪个。 - 设置中间件和增强器的过程可能会让人困惑,特别是如果你试图添加几个配置。
- Redux DevTools 扩展文档最初建议使用一些手写的代码来检查全局命名空间以查看扩展是否可用。许多用户复制和粘贴这些片段,这使得设置代码更难阅读。
使用 configureStore
简化存储设置
configureStore
通过以下方式帮助解决这些问题:
- 有一个带有 "命名" 参数的选项对象,这可能更容易阅读
- 让你提供你想要添加到存储的中间件和增强器的数组,并自动为你调用
applyMiddleware
和compose
- 自动启用 Redux DevTools 扩展
此外,configureStore
默认添加了一些中间件,每个中间件都有一个特定的目标:
redux-thunk
是用于处理组件外部的同步和异步逻辑的最常用的中间件- 在开发中,检查常见错误的中间件,如改变状态或使用非序列化值。
这意味着存储设置代码本身更短,更容易阅读,而且你也可以得到良好的默认行为。
使用它最简单的方式是将根 reducer 函数作为一个名为 reducer
的参数传递:
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer,
})
export default store
你也可以传递一个充满 "slice reducers" 的 对象,configureStore
将为你调用 combineReducers
:
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
})
export default store
注意,这只适用于一级的 reducers。如果你想要嵌套 reducers,你需要自己调用 combineReducers
来处理嵌套。
如果你需要自定义存储设置,你 可以传递额外的选项。以下是使用 Redux Toolkit 的热重载示例可能的样子:
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
preloadedState,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(monitorReducersEnhancer),
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
如果你提供了 middleware
参数,configureStore
将只使用你列出的任何中间件。
如果你想要有一些自定义的中间件 和 所有的默认值一起,你可以使用回调表示法,
调用 getDefaultMiddleware
并在你返回的 middleware
数组中包含结果。
编写 Reducers
Reducers 是 Redux 最重要的概念。典型的 reducer 函数需要:
- 查看 action 对象的
type
字段以了解如何响应 - 通过复制需要更改的状态部分并仅修改这些副本,以不可变的方式更新其状态
虽然你可以在 reducer 中使用任何你想要的条件逻辑,但最常见的方法是 switch
语句,因为它是处理单个字段的多个可能值的直接方式。然而,许多人不喜欢 switch 语句。Redux 文档展示了一个编写一个基于 action 类型的查找表函数的例子,但将自定义该函数的方式留给了用户。
编写 reducers 的其他常见痛点与不可变地更新状态有关。JavaScript 是一种可变语言,手动更新嵌套的不可变数据很难,而且很容易犯错 误。
使用 createReducer
简化 Reducers
由于 "查找表" 方法很受欢迎,Redux Toolkit 包含了一个类似于 Redux 文档中显示的 createReducer
函数。然而,我们的 createReducer
实用程序有一些特殊的 "魔法",使它更好。它内部使用 Immer 库,该库让你编写 "改变" 一些数据的代码,但实际上以不可变的方式应用更新。这使得在 reducer 中意外地改变状态几乎不可能。
一般来说,任何使用 switch
语句的 Redux reducer 都可以直接转换为使用 createReducer
。switch 中的每个 case
都成为传递给 createReducer
的对象中的一个键。不可变的更新逻辑,如扩展对象或复制数组,可能可以转换为直接的 "突变"。也可以保持不可变的更新原样,并返回更新的副本。
以下是一些如何使用 createReducer
的例子。我们从一个使用 switch 语句和不可变更新的典型 "待办事项列表" reducer 开始:
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return state.concat(action.payload)
}
case 'TOGGLE_TODO': {
const { index } = action.payload
return state.map((todo, i) => {
if (i !== index) return todo
return {
...todo,
completed: !todo.completed,
}
})
}
case 'REMOVE_TODO': {
return state.filter((todo, i) => i !== action.payload.index)
}
default:
return state
}
}
注意,我们特别调用 state.concat()
来返回一个带有新待办事项条目的复制数组,state.map()
来返回切换案例的复制数组,并使用对象扩展运算符来复制需要更新的待办事项。
使用 createReducer
,我们可以大大缩短这个例子:
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// 通过调用 push() "改变"数组
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// 通过覆盖字段 "改变"对象
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// 如果我们想要,仍然可以返回一个不可变更新的值
return state.filter((todo, i) => i !== action.payload.index)
})
})
当试图更新深层嵌套的状态时,"改变"状态的能力特别有用。这段复杂而痛苦的代码:
case "UPDATE_VALUE":
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
可以简化为:
updateValue(state, action) {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
}
好多了!
使用 createReducer
的注意事项
虽然 Redux Toolkit 的 createReducer
函数非常有用,但请记住:
- "突变"代码只在我们的
createReducer
函数内部正确工作 - Immer 不会让你混合 "突变" 草稿状态并返回新的状态值
有关更多详细信息,请参阅 createReducer
API 参考。
编写 Action 创建器
Redux 鼓励你编写 "action 创建器" 函数,封装创建 action 对象的过程。虽然这不是严格要求的,但它是 Redux 使用的标准部分。
大多数 action 创建器非常简单。它们接受一些参数,并返回一个带有特定 type
字段和 action 内部参数的 action 对象。这些参数通常放在一个名为 payload
的字段中,这是 Flux Standard Action 约定组织 action 对象内容的一部分。典型的 action 创建器可能看起来像这样:
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}
使用 createAction
定义 Action 创建器
手动编写 action 创建器可能会变得乏味。Redux Toolkit 提供了一个名为 createAction
的函数,它简单地生成一个使用给定 action 类型的 action 创建器,并将其参数转换为 payload
字段:
const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
createAction
还接受一个 "prepare callback" 参数,允许你自定义结果 payload
字段并可选地添加一个 meta
字段。有关使用 prepare callback 定义 action 创建器的详细信息,请参阅 createAction
API 参考。
使用 Action 创建器作为 Action 类型
Redux reducers 需要查找特定的 action 类型,以确定它们应如何更新其状态。通常,这是通过分别定义 action 类型字符串和 action 创建器函数来完成的。Redux Toolkit 的 createAction
函数使这更容易,通过在 action 创建器上定义 action 类型作为 type
字段。
const actionCreator = createAction('SOME_ACTION_TYPE')
console.log(actionCreator.type)
// "SOME_ACTION_TYPE"
const reducer = createReducer({}, (builder) => {
// 如果你使用 TypeScript,action 类型将被正确推断
builder.addCase(actionCreator, (state, action) => {})
// 或者,你可以引用 .type 字段:
// 如 果使用 TypeScript,无法以这种方式推断 action 类型
builder.addCase(actionCreator.type, (state, action) => {})
})
这意味着你不必编写或使用单独的 action 类型变量,或重复 action 类型的名称和值,如 const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"
。
如果你想在 switch 语句中使用其中一个 action 创建器,你需要自己引用 actionCreator.type
:
const actionCreator = createAction('SOME_ACTION_TYPE')
const reducer = (state = {}, action) => {
switch (action.type) {
// 错误:这将无法正确工作!
case actionCreator: {
break
}
// 正确:这将按预期工作
case actionCreator.type: {
break
}
}
}
创建状态切片
Redux 状态通常按照 "切片" 组织,这些切片由传递给 combineReducers
的 reducers 定义:
import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
})
在这个例子中,users
和 posts
都被认为是 "切片"。这两个 reducers 都:
- "拥有" 一部分状态,包括其初始值是什么
- 定义了如何更新该状态
- 定义了哪些特定的 actions 会导致状态更新
常见的做法是在其自己的文件中定义一个切片的 reducer 函数,而在第二个文件中定义 action 创建器。因为这两个函数都需要引用相同的 action 类型,所以这些类型通常在第三个文件中定义并在两个地方都导入:
// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// 省略实现
}
default:
return state
}
}
这里唯一真正必要的部分是 reducer 本身。考虑其他部分:
- 我们可以在两个地方都将 action 类型写为内联字符串
- action 创建器很好,但它们并不是使用 Redux 的 必要条件 - 组件可以跳过提供
mapDispatch
参数给connect
,并直接调用this.props.dispatch({type : "CREATE_POST", payload : {id : 123, title : "Hello World"}})
- 我们甚至写多个文件的唯一原因是因为常见的做法是按照代码的功能进行分离
"ducks" 文件结构 提议将给定切片的所有 Redux 相关逻辑放入一个单独的文件,像这样:
// postsDuck.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST
'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// 省略实际代码
break
}
default:
return state
}
}
这简化了事情,因为我们不需要有多个文件,我们可以删除 action 类型常量的冗余导入。但是,我们仍然需要手动编写 action 类型和 action 创建器。
在对象中定义函数
在现代 JavaScript 中,有几种合法的方式在对象中定义键和函数(这并不特定于 Redux),你可以混合匹配不同的键定义和函数定义。例如,这些都是在对象内部定义函数的合法方式:
const keyName = "ADD_TODO4";
const reducerObject = {
// 显式引号用于键名,箭头函数用于 reducer
"ADD_TODO1" : (state, action) => { }
// 没有引号的裸键,function 关键字
ADD_TODO2 : function(state, action){ }
// 对象字面量函数简写
ADD_TODO3(state, action) { }
// 计算属性
[keyName] : (state, action) => { }
}
使用 "对象字面量函数简写" 可能是最短的代码,但请随意使用你想要的那种方法。
使用 createSlice
简化切片
为了简化这个过程,Redux Toolkit 包含了一个 createSlice
函数,它会根据你提供的 reducer 函数的名称自动生成 action 类型和 action 创建器。
以下是使用 createSlice
的帖子示例:
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
console.log(postsSlice)
/*
{
name: 'posts',
actions : {
createPost,
updatePost,
deletePost,
},
reducer
}
*/
const { createPost } = postsSlice.actions
console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
createSlice
查看了在 reducers
字段中定义的所有函数,对于提供的每一个 "case reducer" 函数,生成一个使用 reducer 名称作为 action 类型本身的 action 创建器。所以,createPost
reducer 成为了一个 action 类型为 "posts/createPost"
,并且 createPost()
action 创建器将返回一个具有该类型的 action。
导出和使用切片
大多数时候,你会想要定义一个切片,并导出它的动作创建器和reducers。推荐的方式是使用ES6的解构和导出语法:
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
// 提取动作创建器对象和reducer
const { actions, reducer } = postsSlice
// 通过名称提取并导出每个动作创建器
export const { createPost, updatePost, deletePost } = actions
// 导出reducer,可以作为默认或命名导出
export default reducer
如果你愿意,也可以直接导出切片对象本身。
这样定义的切片在概念上非常类似于"Redux Ducks"模式用于定义和导出动作创建器和reducers。然而,在导入和导出切片时,需要注意一些可能的问题。
首先,Redux动作类型并不是专属于单个切片。从概念上讲,每个切片reducer "拥有" 它自己的Redux状态部分,但它应该能够监听任何动作类型并适当地更新其状态。例如,许多不同的切片可能希望通过清除数据或重置为初始状态值来响应 "用户注销" 动作。在设计你的状态形状和创建 你的切片时,请记住这一点。
其次,如果两个模块试图导入彼此,JS模块可能会有 "循环引用" 问题。这可能导致导入的内容未定义,这可能会破坏需要该导入的代码。特别是在 "ducks" 或切片的情况下,如果在两个不同的文件中定义的切片都想响应在另一个文件中定义的动作,就可能发生这种情况。
这个CodeSandbox示例演示了这个问题:
如果你遇到这个问题,你可能需要以避免循环引用的方式重构你的代码。这通常需要将共享代码提取到一个独立的公共文件中,这样两个模块都可以导入和使用。在这种情况下,你可能会在一个单独的文件中使用 createAction
定义一些公共动作类型,将这些动作创建器导入到每个切片文件中,并使用 extraReducers
参数处理它们。
这篇文章 如何修复JS中的循环依赖问题 提供了额外的信息和示例,可以帮助解决这个问题。
异步逻辑和数据获取
使用中间件启用异步逻辑
本身,Redux存储并不知道任何关于异步逻辑的事情。它只知道如何同步地分派动作,通过调用根reducer函数更新状态,并通知UI有些东西已经改变。任何异步性都必须在存储之外发生。
但是,如果你想让异步逻辑通过分派或检查当前存储状态与存储进行交互呢?这就是Redux中间件的作用。它们扩展了存储,并允许你:
- 当任何动作被分派时执行额外的逻辑(如记录动作和状态)
- 暂停、修改、延迟、替换或阻止分派的动作
- 编写有权访问
dispatch
和getState
的额外代码 - 通过拦截它们并分派真正的动作对象,教
dispatch
如何接受除纯动作对象之外的其他值,如函数和承诺
使用中间件的最常见原因是允许不同类型的异步逻辑与存储进行交互。这允许你编写可以分派动作和检查存储状态的代码,同时将该逻辑与你的UI保持分离。
Redux有许多种异步中间件,每种都让你使用不同的语法编写你的逻辑。最常见的异步中间件有:
redux-thunk
,它让你直接编写可能包含异步逻辑的普通函数redux-saga
,它使用生成器函数返回行为描述,以便中间件可以执行redux-observable
,它使用RxJS可观察库创建处理动作的函数链
Redux Toolkit的 RTK Query数据获取API 是为Redux应用程序专门构建的数据获取和缓存解决方案,可以 消除编写任何thunks或reducers来管理数据获取的需要。我们鼓励你尝试一下,看看它是否可以帮助简化你自己应用程序中的数据获取代码!
如果你确实需要自己编写数据获取逻辑,我们推荐 使用Redux Thunk中间件作为标准方法,因为它足以应对大多数典型的使用场景(如基本的AJAX数据获取)。此外,thunks中 async/await
语法的使用使它们更易于阅读。