跳至主要内容

使用指南

Redux 核心库故意不带任何意见。它允许你决定如何处理所有事情,例如存储设置、状态包含的内容以及如何构建 Reducer。

在某些情况下这很好,因为它提供了灵活性,但这种灵活性并不总是需要的。有时我们只需要最简单的方法来入门,并且希望开箱即用地获得一些良好的默认行为。或者,也许你正在编写一个更大的应用程序,发现自己编写了一些类似的代码,并且希望减少需要手动编写的代码量。

快速入门 页面所述,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 通过以下方式帮助解决这些问题

  • 拥有一个带有“命名”参数的选项对象,这更容易阅读
  • 允许您提供要添加到存储的中间件和增强器数组,并自动为您调用 applyMiddlewarecompose
  • 自动启用 Redux DevTools 扩展

此外,configureStore 默认添加了一些中间件,每个中间件都有特定的目标

  • redux-thunk 是最常用的中间件,用于处理组件外部的同步和异步逻辑
  • 在开发中,中间件会检查常见的错误,例如修改状态或使用不可序列化值。

这意味着存储设置代码本身更短,更容易阅读,而且您还可以获得开箱即用的良好默认行为。

最简单的使用方法是将根 reducer 函数作为名为 reducer 的参数传递

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({
reducer: rootReducer,
})

export default store

您还可以传递一个包含 "切片 reducer" 的对象,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

请注意,这仅适用于一层 reducer。如果您想嵌套 reducer,则需要自己调用 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 数组中。

编写 Reducer

Reducer 是 Redux 中最重要的概念。典型的 reducer 函数需要

  • 查看操作对象的 type 字段,以了解它应该如何响应
  • 通过复制需要更改的状态部分并仅修改这些副本,以不可变的方式更新其状态

虽然您可以在 reducer 中使用任何您想要的条件逻辑,但最常见的方法是使用switch语句,因为它是一种处理单个字段的多个可能值的直接方法。但是,许多人不喜欢 switch 语句。Redux 文档展示了编写一个基于操作类型的查找表的函数的示例,但留给用户自己自定义该函数。

编写 reducer 的其他常见痛点与不可变地更新状态有关。JavaScript 是一种可变语言,手动更新嵌套的不可变数据很困难,并且很容易出错。

使用createReducer简化 Reducer

由于“查找表”方法很流行,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) => {
// "mutate" the array by calling push()
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// "mutate" the object by overwriting a field
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
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 参考

编写动作创建者

Redux鼓励您编写“动作创建者”函数,这些函数封装了创建动作对象的流程。虽然这不是严格要求的,但它是Redux使用的标准部分。

大多数动作创建者非常简单。它们接受一些参数,并返回一个具有特定type字段和参数的动作对象。这些参数通常放在名为payload的字段中,它是Flux Standard Action约定的一部分,用于组织动作对象的内容。一个典型的动作创建者可能看起来像

function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}

使用createAction定义动作创建者

手动编写动作创建者可能会很繁琐。Redux Toolkit提供了一个名为createAction的函数,它只是生成一个使用给定动作类型的动作创建者,并将它的参数转换为payload字段。

const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})

createAction还接受一个“准备回调”参数,它允许您自定义生成的payload字段,并可选地添加meta字段。有关使用准备回调定义动作创建者的详细信息,请参见createAction API 参考

使用 Action Creators 作为 Action 类型

Redux reducers 需要查找特定的 action 类型来确定它们应该如何更新其状态。通常,这是通过分别定义 action 类型字符串和 action creator 函数来完成的。Redux Toolkit 的 createAction 函数通过将 action 类型定义为 action creator 上的 type 字段,使这变得更容易。

const actionCreator = createAction('SOME_ACTION_TYPE')

console.log(actionCreator.type)
// "SOME_ACTION_TYPE"

const reducer = createReducer({}, (builder) => {
// if you use TypeScript, the action type will be correctly inferred
builder.addCase(actionCreator, (state, action) => {})

// Or, you can reference the .type field:
// if using TypeScript, the action type cannot be inferred that way
builder.addCase(actionCreator.type, (state, action) => {})
})

这意味着您不必编写或使用单独的 action 类型变量,也不必重复 action 类型的名称和值,例如 const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"

如果您想在 switch 语句中使用其中一个 action creator,您需要自己引用 actionCreator.type

const actionCreator = createAction('SOME_ACTION_TYPE')

const reducer = (state = {}, action) => {
switch (action.type) {
// ERROR: this won't work correctly!
case actionCreator: {
break
}
// CORRECT: this will work as expected
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,
})

在这个例子中,usersposts 都被认为是“切片”。这两个 reducers

  • "拥有" 状态的一部分,包括初始值是什么
  • 定义如何更新该状态
  • 定义哪些特定的 actions 会导致状态更新

常见的做法是在单独的文件中定义切片的 reducer 函数,在另一个文件中定义 action creators。由于这两个函数都需要引用相同的 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: {
// omit implementation
}
default:
return state
}
}

这里唯一真正必要的部分是 reducer 本身。考虑其他部分

  • 我们可以在两个地方将 action 类型写成内联字符串
  • action creators 很好,但它们并不需要使用 Redux - 组件可以跳过向 connect 提供 mapDispatch 参数,并直接调用 this.props.dispatch({type : "CREATE_POST", payload : {id : 123, title : "Hello World"}}) 本身
  • 我们甚至编写多个文件的唯一原因是,通常会根据功能将代码分开

The "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: {
// Omit actual code
break
}
default:
return state
}
}

这简化了事情,因为我们不需要有多个文件,并且可以删除对 action 类型常量的冗余导入。但是,我们仍然需要手动编写 action 类型和 action creators。

在对象中定义函数

在现代 JavaScript 中,有几种合法的方法可以在对象中定义键和函数(这与 Redux 无关),并且可以混合匹配不同的键定义和函数定义。例如,以下都是定义对象内部函数的合法方法

const keyName = "ADD_TODO4";

const reducerObject = {
// Explicit quotes for the key name, arrow function for the reducer
"ADD_TODO1" : (state, action) => { }

// Bare key with no quotes, function keyword
ADD_TODO2 : function(state, action){ }

// Object literal function shorthand
ADD_TODO3(state, action) { }

// Computed property
[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”函数生成一个 action 创建器,该创建器使用 reducer 的名称作为 action 类型本身。因此,createPost reducer 成为 "posts/createPost" 的 action 类型,而 createPost() action 创建器将返回一个具有该类型的 action。

导出和使用切片

大多数情况下,你希望定义一个切片,并导出它的 action 创建器和 reducer。推荐使用 ES6 解构和导出语法来实现这一点。

const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})

// Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer

如果你愿意,也可以直接导出切片对象本身。

这样定义的切片在概念上与 "Redux Ducks" 模式 非常相似,用于定义和导出 action 创建器和 reducer。但是,在导入和导出切片时,需要注意一些潜在的缺点。

首先,**Redux action 类型不应局限于单个切片**。从概念上讲,每个切片 reducer “拥有” Redux 状态的一部分,但它应该能够监听任何 action 类型并相应地更新其状态。例如,许多不同的切片可能希望响应“用户注销” action,通过清除数据或重置回初始状态值。在设计状态形状和创建切片时,请牢记这一点。

其次,**如果两个模块尝试相互导入,JS 模块可能会出现“循环引用”问题**。这会导致导入未定义,这可能会破坏需要该导入的代码。特别是在“ducks”或切片的情况下,如果在两个不同文件中定义的切片都希望响应在另一个文件中定义的 action,就会发生这种情况。

此 CodeSandbox 示例演示了问题。

如果您遇到此问题,您可能需要以避免循环引用方式重构代码。这通常需要将共享代码提取到一个单独的公共文件中,这两个模块都可以导入和使用。在这种情况下,您可以在一个单独的文件中使用 `createAction` 定义一些公共操作类型,将这些操作创建者导入到每个切片文件中,并使用 `extraReducers` 参数处理它们。

文章 如何在 JS 中修复循环依赖问题 提供了更多信息和示例,可以帮助解决此问题。

异步逻辑和数据获取

使用中间件启用异步逻辑

Redux 存储本身不知道任何关于异步逻辑的信息。它只知道如何同步地调度操作,通过调用根 reducer 函数更新状态,并通知 UI 发生了变化。任何异步操作都必须在存储之外进行。

但是,如果您想让异步逻辑通过调度或检查当前存储状态与存储交互呢?这就是 Redux 中间件 的用武之地。它们扩展了存储,并允许您

  • 在调度任何操作时执行额外的逻辑(例如记录操作和状态)
  • 暂停、修改、延迟、替换或停止已调度操作
  • 编写可以访问 `dispatch` 和 `getState` 的额外代码
  • 通过拦截它们并调度真正的操作对象,来教 `dispatch` 如何接受除了普通操作对象之外的其他值,例如函数和 Promise

使用中间件最常见的原因是允许不同类型的异步逻辑与存储交互。这使您可以编写可以调度操作并检查存储状态的代码,同时将该逻辑与 UI 分开。

有很多种用于 Redux 的异步中间件,每种都允许您使用不同的语法编写逻辑。最常见的异步中间件是

  • redux-thunk,它允许您编写可能包含异步逻辑的普通函数
  • redux-saga,它使用生成器函数返回行为描述,以便它们可以由中间件执行
  • redux-observable,它使用 RxJS 可观察库来创建处理操作的函数链

这些库都有不同的用例和权衡。.

提示

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

如果您确实需要自己编写数据获取逻辑,我们建议 使用 Redux Thunk 中间件作为标准方法,因为它足以满足大多数典型用例(例如基本 AJAX 数据获取)。此外,在 thunk 中使用 async/await 语法使它们更容易阅读。

Redux Toolkit 的 configureStore 函数 默认情况下会自动设置 thunk 中间件,因此您可以立即开始将 thunk 作为应用程序代码的一部分编写。

在切片中定义异步逻辑

Redux Toolkit 目前没有提供任何用于编写 thunk 函数的特殊 API 或语法。特别是,它们不能作为 createSlice() 调用的一部分定义。您必须将它们与 reducer 逻辑分开编写,与普通 Redux 代码完全相同。

Thunk 通常会分派普通操作,例如 dispatch(dataLoaded(response.data))

许多 Redux 应用程序使用“按类型分文件夹”的方法来组织代码。在这种结构中,thunk 操作创建者通常与普通操作创建者一起定义在“actions”文件中。

由于我们没有单独的“actions”文件,将这些thunk直接写入我们的“slice”文件中是有意义的。这样,它们就可以访问切片中的普通action creators,并且很容易找到thunk函数所在的位置。

包含thunk的典型切片文件如下所示

// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: [],
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
},
},
})

// Destructure and export the plain action creators
export const { usersLoading, usersReceived } = usersSlice.actions

// Define a thunk that dispatches those action creators
const fetchUsers = () => async (dispatch) => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}

Redux 数据获取模式

Redux 的数据获取逻辑通常遵循一种可预测的模式

  • 在请求之前会分派一个“start”操作,以指示请求正在进行中。这可以用来跟踪加载状态,允许跳过重复请求,或在 UI 中显示加载指示器。
  • 进行异步请求
  • 根据请求结果,异步逻辑会分派一个包含结果数据的“success”操作,或一个包含错误详细信息的“failure”操作。在两种情况下,reducer 逻辑都会清除加载状态,并处理来自成功情况的结果数据,或存储错误值以供潜在显示。

这些步骤不是必需的,但在 Redux 教程中作为建议模式推荐

典型的实现可能如下所示

const getRepoDetailsStarted = () => ({
type: 'repoDetails/fetchStarted',
})
const getRepoDetailsSuccess = (repoDetails) => ({
type: 'repoDetails/fetchSucceeded',
payload: repoDetails,
})
const getRepoDetailsFailed = (error) => ({
type: 'repoDetails/fetchFailed',
error,
})
const fetchIssuesCount = (org, repo) => async (dispatch) => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}

但是,使用这种方法编写代码很繁琐。每种类型的请求都需要重复类似的实现

  • 需要为三种不同的情况定义唯一的操作类型
  • 每种操作类型通常都有一个相应的操作创建器函数
  • 需要编写一个thunk,它以正确的顺序分派正确的操作

createAsyncThunk 通过生成操作类型和操作创建器以及生成一个分派这些操作的thunk来抽象这种模式。

使用 createAsyncThunk 进行异步请求

作为开发人员,您可能最关心的是进行 API 请求所需的实际逻辑,Redux 操作历史记录日志中显示的操作类型名称,以及您的 reducer 如何处理获取的数据。定义多个操作类型和以正确的顺序分派操作的重复细节并不重要。

createAsyncThunk 简化了此过程 - 您只需要提供操作类型前缀的字符串和一个有效载荷创建器回调,该回调执行实际的异步逻辑并返回包含结果的 promise。作为回报,createAsyncThunk 将为您提供一个thunk,该thunk将根据您返回的 promise 和您可以在 reducer 中处理的操作类型来处理分派正确的操作

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
)

// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

thunk 操作创建器接受一个参数,该参数将作为第一个参数传递给您的有效载荷创建器回调。

有效载荷创建者还会收到一个thunkAPI对象,其中包含通常传递给标准 Redux thunk 函数的参数,以及一个自动生成的唯一随机请求 ID 字符串和一个AbortController.signal对象

interface ThunkAPI {
dispatch: Function
getState: Function
extra?: any
requestId: string
signal: AbortSignal
}

您可以在有效载荷回调中根据需要使用这些参数来确定最终结果。

管理规范化数据

大多数应用程序通常处理深度嵌套或关系型数据。规范化数据的目的是有效地组织状态中的数据。这通常通过将集合存储为以id为键的对象,同时存储这些id的排序数组来完成。有关更深入的解释和更多示例,Redux 文档页面上有一个关于“规范化状态形状”的优秀参考.

手动规范化

规范化数据不需要任何特殊的库。以下是一个基本示例,说明如何使用一些手写的逻辑来规范来自返回形状为{ users: [{id: 1, first_name: 'normalized', last_name: 'person'}] }fetchAll API 请求的响应。

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import userAPI from './userAPI'

export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
return response.data
})

export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
// reduce the collection by the id property into a shape of { 1: { ...user }}
const byId = action.payload.users.reduce((byId, user) => {
byId[user.id] = user
return byId
}, {})
state.entities = byId
state.ids = Object.keys(byId)
})
},
})

虽然我们能够编写此代码,但它确实会变得重复,尤其是在处理多种类型的数据时。此外,此示例仅处理将条目加载到状态中,而不处理更新它们。

使用normalizr规范化

normalizr 是一个流行的现有库,用于规范化数据。您可以在没有 Redux 的情况下单独使用它,但它非常常用于 Redux。典型的用法是对来自 API 响应的集合进行格式化,然后在您的 reducer 中处理它们。

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { normalize, schema } from 'normalizr'

import userAPI from './userAPI'

const userEntity = new schema.Entity('users')

export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// Normalize the data before passing it to our reducer
const normalized = normalize(response.data, [userEntity])
return normalized.entities
})

export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.entities = action.payload.users
state.ids = Object.keys(action.payload.users)
})
},
})

与手写版本一样,这不会处理将其他条目添加到状态中,也不会处理稍后更新它们 - 它只是加载接收到的所有内容。

使用 createEntityAdapter 进行规范化

Redux Toolkit 的 createEntityAdapter API 提供了一种标准化的方式来存储切片中的数据,它将集合转换为 { ids: [], entities: {} } 的形状。除了这种预定义的状态形状,它还生成了一组知道如何处理数据的 reducer 函数和选择器。

import {
createSlice,
createAsyncThunk,
createEntityAdapter,
} from '@reduxjs/toolkit'
import userAPI from './userAPI'

export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data
})

export const updateUser = createAsyncThunk('users/updateOne', async (arg) => {
const response = await userAPI.updateUser(arg)
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data
})

export const usersAdapter = createEntityAdapter()

// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
// If you want to track 'loading' or other keys, you would initialize them here:
// `getInitialState({ loading: false, activeRequestId: null })`
const initialState = usersAdapter.getInitialState()

export const slice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload
usersAdapter.updateOne(state, { id, changes })
})
},
})

const reducer = slice.reducer
export default reducer

export const { removeUser } = slice.actions

您可以 在 CodeSandbox 上查看此示例用法的完整代码

createEntityAdapter 与规范化库一起使用

如果您已经在使用 normalizr 或其他规范化库,您可以考虑将其与 createEntityAdapter 一起使用。为了扩展上面的示例,这里演示了如何使用 normalizr 格式化有效负载,然后利用 createEntityAdapter 提供的实用程序。

默认情况下,setAlladdManyupsertMany CRUD 方法期望一个实体数组。但是,它们也允许您传入一个形状为 { 1: { id: 1, ... }} 的对象作为替代,这使得插入预规范化数据变得更容易。

// features/articles/articlesSlice.js
import {
createSlice,
createEntityAdapter,
createAsyncThunk,
createSelector,
} from '@reduxjs/toolkit'
import fakeAPI from '../../services/fakeAPI'
import { normalize, schema } from 'normalizr'

// Define normalizr entity schemas
export const userEntity = new schema.Entity('users')
export const commentEntity = new schema.Entity('comments', {
commenter: userEntity,
})
export const articleEntity = new schema.Entity('articles', {
author: userEntity,
comments: [commentEntity],
})

const articlesAdapter = createEntityAdapter()

export const fetchArticle = createAsyncThunk(
'articles/fetchArticle',
async (id) => {
const data = await fakeAPI.articles.show(id)
// Normalize the data so reducers can load a predictable payload, like:
// `action.payload = { users: {}, articles: {}, comments: {} }`
const normalized = normalize(data, articleEntity)
return normalized.entities
}
)

export const slice = createSlice({
name: 'articles',
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Handle the fetch result by inserting the articles here
articlesAdapter.upsertMany(state, action.payload.articles)
})
},
})

const reducer = slice.reducer
export default reducer

// features/users/usersSlice.js

import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'

const usersAdapter = createEntityAdapter()

export const slice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// And handle the same fetch result by inserting the users here
usersAdapter.upsertMany(state, action.payload.users)
})
},
})

const reducer = slice.reducer
export default reducer

// features/comments/commentsSlice.js

import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'

const commentsAdapter = createEntityAdapter()

export const slice = createSlice({
name: 'comments',
initialState: commentsAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Same for the comments
commentsAdapter.upsertMany(state, action.payload.comments)
})
},
})

const reducer = slice.reducer
export default reducer

您可以 在 CodeSandbox 上查看此示例 normalizr 用法的完整代码

使用 createEntityAdapter 的选择器

实体适配器提供了一个选择器工厂,它为您生成最常用的选择器。以上面的示例为例,我们可以像这样向 usersSlice 添加选择器

// Rename the exports for readability in component usage
export const {
selectById: selectUserById,
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectTotalUsers,
} = usersAdapter.getSelectors((state) => state.users)

然后,您可以在组件中使用这些选择器,例如

import React from 'react'
import { useSelector } from 'react-redux'
import { selectTotalUsers, selectAllUsers } from './usersSlice'

import styles from './UsersList.module.css'

export function UsersList() {
const count = useSelector(selectTotalUsers)
const users = useSelector(selectAllUsers)

return (
<div>
<div className={styles.row}>
There are <span className={styles.value}>{count}</span> users.{' '}
{count === 0 && `Why don't you fetch some more?`}
</div>
{users.map((user) => (
<div key={user.id}>
<div>{`${user.first_name} ${user.last_name}`}</div>
</div>
))}
</div>
)
}

指定备用 ID 字段

默认情况下,createEntityAdapter 假设您的数据在 entity.id 字段中具有唯一的 ID。如果您的数据集将 ID 存储在不同的字段中,您可以传入一个 selectId 参数,该参数返回相应的字段。

// In this instance, our user data always has a primary key of `idx`
const userData = {
users: [
{ idx: 1, first_name: 'Test' },
{ idx: 2, first_name: 'Two' },
],
}

// Since our primary key is `idx` and not `id`,
// pass in an ID selector to return that field instead
export const usersAdapter = createEntityAdapter({
selectId: (user) => user.idx,
})

排序实体

createEntityAdapter 提供了一个 sortComparer 参数,您可以利用它来对状态中的 ids 集合进行排序。当您想要保证排序顺序并且您的数据没有预先排序时,这非常有用。

// In this instance, our user data always has a primary key of `id`, so we do not need to provide `selectId`.
const userData = {
users: [
{ id: 1, first_name: 'Test' },
{ id: 2, first_name: 'Banana' },
],
}

// Sort by `first_name`. `state.ids` would be ordered as
// `ids: [ 2, 1 ]`, since 'B' comes before 'T'.
// When using the provided `selectAll` selector, the result would be sorted:
// [{ id: 2, first_name: 'Banana' }, { id: 1, first_name: 'Test' }]
export const usersAdapter = createEntityAdapter({
sortComparer: (a, b) => a.first_name.localeCompare(b.first_name),
})

处理不可序列化数据

Redux 的核心使用原则之一是 您不应该将不可序列化值放入状态或操作中

然而,就像大多数规则一样,也有一些例外。在某些情况下,您可能需要处理需要接受非序列化数据的操作。这应该非常少见,只有在必要时才进行,并且这些非序列化有效负载不应该通过 reducer 进入您的应用程序状态。

可序列化开发检查中间件 会在检测到您的操作或状态中的非序列化值时自动发出警告。我们鼓励您保持此中间件处于活动状态,以帮助避免意外出错。但是,如果您确实需要关闭这些警告,可以通过配置它来忽略特定操作类型或操作和状态中的字段来自定义中间件。

configureStore({
//...
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['your/action/type'],
// Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}),
})

与 Redux-Persist 一起使用

如果使用 Redux-Persist,您应该明确忽略它分发的所有操作类型。

import { configureStore } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'

import App from './App'
import rootReducer from './reducers'

const persistConfig = {
key: 'root',
version: 1,
storage,
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})

let persistor = persistStore(store)

ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root'),
)

此外,您可以通过向您希望在调用 persistor.purge() 时清除的特定切片添加额外的 reducer 来清除任何持久化的状态。当您希望在分发的注销操作时清除持久化的状态时,这特别有用。

import { PURGE } from "redux-persist";

...
extraReducers: (builder) => {
builder.addCase(PURGE, (state) => {
customEntityAdapter.removeAll(state);
});
}

强烈建议将您使用 RTK Query 配置的任何 api(s) 列入黑名单。如果 api 切片 reducer 未列入黑名单,api 缓存将自动持久化和恢复,这可能会导致来自不再存在的组件的幽灵订阅。配置此操作应类似于以下内容。

const persistConfig = {
key: 'root',
version: 1,
storage,
blacklist: [pokemonApi.reducerPath],
}

请参阅 Redux Toolkit #121: 如何与 Redux-Persist 一起使用?Redux-Persist #988: 非序列化值错误 以获取更多讨论。

与 React-Redux-Firebase 一起使用

RRF 在大多数操作和状态中包含时间戳值,从 3.x 版本开始,但有一些 PR 可能会在 4.x 版本中改进该行为。

与该行为一起使用的可能配置可能如下所示。

import { configureStore } from '@reduxjs/toolkit'
import {
getFirebase,
actionTypes as rrfActionTypes,
} from 'react-redux-firebase'
import { constants as rfConstants } from 'redux-firestore'
import rootReducer from './rootReducer'

const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [
// just ignore every redux-firebase and react-redux-firebase action type
...Object.keys(rfConstants.actionTypes).map(
(type) => `${rfConstants.actionsPrefix}/${type}`,
),
...Object.keys(rrfActionTypes).map(
(type) => `@@reactReduxFirebase/${type}`,
),
],
ignoredPaths: ['firebase', 'firestore'],
},
thunk: {
extraArgument: {
getFirebase,
},
},
}),
})

export default store