跳至主要内容

迁移到 RTK 2.0 和 Redux 5.0

您将学到什么
  • Redux Toolkit 2.0、Redux 核心 5.0、Reselect 5.0 和 Redux Thunk 3.0 中的变化,包括重大变更和新功能

简介

Redux Toolkit 自 2019 年起推出,如今已成为编写 Redux 应用程序的标准方式。我们已经 4 年多没有进行任何重大变更。现在,RTK 2.0 为我们提供了一个机会来现代化打包,清理已弃用的选项,并收紧一些边缘情况。

Redux Toolkit 2.0 伴随着所有其他 Redux 包的主要版本:Redux 核心 5.0、React-Redux 9.0、Reselect 5.0 和 Redux Thunk 3.0.

此页面列出了这些包中已知的潜在重大变更,以及 Redux Toolkit 2.0 中的新功能。提醒一下,**您实际上不需要直接安装或使用核心 redux 包** - RTK 会包装它,并重新导出所有方法和类型。

实际上,**大多数“重大”变更不会对最终用户产生实际影响,我们预计许多项目只需更新包版本,只需进行很少的代码更改**。

最有可能需要应用程序代码更新的变更是

打包变更(全部)

我们对所有 Redux 相关库的构建打包进行了更新。这些在技术上是“重大”的,但应该对最终用户透明,并且实际上能够更好地支持在 Node 下使用 ESM 文件等场景。

package.json 中添加 exports 字段

我们已将包定义迁移到包含 exports 字段以定义要加载的工件,其中现代 ESM 构建作为主要工件(出于兼容性目的,仍然包含 CJS)。

我们已经对该软件包进行了本地测试,但我们希望社区在您自己的项目中试用它,并报告您发现的任何问题!

构建工件现代化

我们已通过多种方式更新了构建输出

  • 构建输出不再进行转译! 相反,我们针对现代 JS 语法(ES2020)
  • 将所有构建工件移至 ./dist/ 下,而不是单独的顶级文件夹
  • 我们测试的最低 Typescript 版本现在是 TS 4.7

放弃 UMD 构建

Redux 一直以来都附带 UMD 构建工件。这些主要用于作为脚本标签直接导入,例如在 CodePen 或无捆绑器构建环境中。

目前,我们正在从已发布的软件包中删除这些构建工件,因为这些用例在今天似乎非常罕见。

我们确实在 dist/$PACKAGE_NAME.browser.mjs 中包含了一个浏览器就绪的 ESM 构建工件,可以通过指向 Unpkg 上该文件的脚本标签加载。

如果您有强烈的用例需要我们继续包含 UMD 构建工件,请告诉我们!

重大变更

核心

操作类型 必须 是字符串

我们一直明确地告诉用户,操作和状态 必须 是可序列化的,并且 action.type 应该 是一个字符串。这样做既是为了确保操作是可序列化的,也有助于在 Redux DevTools 中提供可读的操作历史记录。

store.dispatch(action) 现在明确地强制执行 action.type 必须 是一个字符串,如果它不是字符串,则会抛出错误,就像它在操作不是普通对象时抛出错误一样。

实际上,这在 99.99% 的情况下已经成立,对用户不会有任何影响(尤其是那些使用 Redux Toolkit 和 createSlice 的用户),但可能有一些遗留的 Redux 代码库选择使用 Symbols 作为操作类型。

createStore 已弃用

Redux 4.2.0 中,我们标记了原始的 createStore 方法为 @deprecated。严格来说,不是一个重大更改,也不是 5.0 中的新功能,但我们在这里为了完整性而记录它。

此弃用仅仅是一个视觉指示器,旨在鼓励用户 从旧版 Redux 模式迁移他们的应用程序以使用现代 Redux Toolkit API.

弃用会导致在导入和使用时出现视觉上的删除线,例如createStore,但不会出现运行时错误或警告

createStore 将继续无限期地工作,并且永远不会被删除。但是,今天我们希望所有 Redux 用户都使用 Redux Toolkit 来处理他们的所有 Redux 逻辑。

要解决此问题,有三种选择

  • 遵循我们的强烈建议,切换到 Redux Toolkit 和 configureStore
  • 什么也不做。它只是一个视觉上的删除线,不会影响代码的行为。忽略它。
  • 切换到使用现在导出的 legacy_createStore API,它与原始函数完全相同,但没有 @deprecated 标签。最简单的选择是进行别名导入重命名,例如 import { legacy_createStore as createStore } from 'redux'

Typescript 重写

在 2019 年,我们开始了一个由社区驱动的将 Redux 代码库转换为 TypeScript 的过程。最初的努力在 #3500: 移植到 TypeScript 中进行了讨论,并且该工作在 PR #3536: 转换为 TypeScript 中进行了整合。

但是,由于担心可能与现有生态系统存在兼容性问题(以及我们方面的普遍惯性),TS 转换后的代码在仓库中闲置了几年,未被使用和发布。

Redux core v5 现在是基于该 TS 转换后的源代码构建的。理论上,这在运行时行为和类型方面应该与 4.x 版本几乎相同,但很可能某些更改会导致类型问题。

如果您遇到任何意外的兼容性问题,请在 Github 上报告!

AnyAction 已被弃用,取而代之的是 UnknownAction

Redux TS 类型一直导出 AnyAction 类型,该类型定义为具有 {type: string} 并将任何其他字段视为 any。这使得编写诸如 console.log(action.whatever) 之类的用法变得容易,但不幸的是,它没有提供任何有意义的类型安全性。

我们现在导出 UnknownAction 类型,该类型将除 action.type 之外的所有字段视为 unknown。这鼓励用户编写类型保护程序,这些程序检查操作对象并断言其特定 TS 类型。在这些检查中,您可以访问具有更好类型安全性的字段。

UnknownAction 现在是 Redux 源代码中任何期望操作对象的地方的默认值。

AnyAction 仍然存在以确保兼容性,但已被标记为已弃用。

请注意,Redux Toolkit 的操作创建者有一个 .match() 方法,它充当有用的类型保护程序

if (todoAdded.match(someUnknownAction)) {
// action is now typed as a PayloadAction<Todo>
}

您还可以使用新的 isAction 实用程序来检查未知值是否为某种操作对象。

Middleware 类型已更改 - 中间件 actionnext 被类型化为 unknown

以前,next 参数被类型化为传递的 D 类型参数,而 action 被类型化为从调度类型中提取的 Action。这些都不是安全的假设

  • next 将被类型化为具有所有调度扩展,包括链中较早的扩展,这些扩展将不再适用。
    • 从技术上讲,将 next 类型化为基本 Redux 存储实现的默认 Dispatch 几乎是安全的,但是这会导致 next(action) 出错(因为我们无法保证 action 实际上是 Action) - 并且它不会考虑任何后续中间件,这些中间件在看到特定操作时返回的不是它们给定的操作。
  • action 不一定是已知操作,它可以是任何东西 - 例如 thunk 将是一个没有 .type 属性的函数(因此 AnyAction 将是不准确的)

我们已将 next 更改为 (action: unknown) => unknown(这是准确的,我们不知道 next 期望什么或将返回什么),并将 action 参数更改为 unknown(如上所述,这是准确的)。

为了安全地与 action 参数中的值交互或访问字段,您必须首先进行类型保护检查以缩小类型,例如 isAction(action)someActionCreator.match(action)

这种新的类型与 v4 Middleware 类型不兼容,因此,如果某个包的中间件表示它不兼容,请检查它从哪个版本的 Redux 获取其类型!(请参阅本页后面的 覆盖依赖项。)

PreloadedState 类型已删除,取而代之的是 Reducer 泛型

我们对 TS 类型进行了一些调整,以提高类型安全性并改善行为。

首先,Reducer 类型现在具有 PreloadedState 可能的泛型

type Reducer<S, A extends Action, PreloadedState = S> = (
state: S | PreloadedState | undefined,
action: A
) => S

根据 #4491 中的解释

为什么需要进行此更改?当存储首次由 createStore/configureStore 创建时,初始状态将设置为作为 preloadedState 参数传递的任何内容(如果未传递任何内容,则为 undefined)。这意味着,在第一次调用 reducer 时,它将使用 preloadedState 进行调用。在第一次调用之后,reducer 将始终传递当前状态(即 S)。

对于大多数普通 reducer,S | undefined 准确地描述了可以传递给 preloadedState 的内容。但是,combineReducers 函数允许使用 Partial<S> | undefined 的预加载状态。

解决方案是使用一个单独的泛型来表示 reducer 接受其预加载状态的内容。这样,createStore 就可以使用该泛型作为其 preloadedState 参数。

以前,这是通过 $CombinedState 类型来处理的,但这使事情变得复杂,并导致了一些用户报告的问题。这完全消除了对 $CombinedState 的需要。

此更改确实包含一些重大更改,但总体而言,对用户升级的影响应该不大。

  • ReducerReducersMapObjectcreateStore/configureStore 类型/函数接受一个额外的 PreloadedState 泛型,该泛型默认为 S
  • combineReducers 的重载已删除,取而代之的是单个函数定义,该定义将 ReducersMapObject 作为其泛型参数。由于这些更改,删除重载是必要的,因为有时它会选择错误的重载。
  • 显式列出 reducer 泛型的增强器需要添加第三个泛型。

工具包专用

createSlice.extraReducerscreateReducer 的对象语法已移除

RTK 的 createReducer API 最初设计为接受一个动作类型字符串到 case reducer 的查找表,例如 { "ADD_TODO": (state, action) => {} }。我们后来添加了“构建器回调”形式,以允许在添加“匹配器”和默认处理程序方面有更大的灵活性,并且对 createSlice.extraReducers 进行了同样的操作。

我们在 RTK 2.0 中删除了 createReducercreateSlice.extraReducers 的“对象”形式,因为构建器回调形式实际上与代码行的数量相同,并且与 TypeScript 的配合效果更好。

例如,这

const todoAdded = createAction('todos/todoAdded')

createReducer(initialState, {
[todoAdded]: (state, action) => {}
})

createSlice({
name,
initialState,
reducers: {
/* case reducers here */
},
extraReducers: {
[todoAdded]: (state, action) => {}
}
})

应该迁移到

createReducer(initialState, builder => {
builder.addCase(todoAdded, (state, action) => {})
})

createSlice({
name,
initialState,
reducers: {
/* case reducers here */
},
extraReducers: builder => {
builder.addCase(todoAdded, (state, action) => {})
}
})
代码修改器

为了简化代码库的升级,我们发布了一组代码修改器,它们将自动将已弃用的“对象”语法转换为等效的“构建器”语法。

代码修改器包在 NPM 上以 @reduxjs/rtk-codemods 的形式提供。更多详细信息请访问 这里

要对您的代码库运行代码修改器,请运行 npx @reduxjs/rtk-codemods <TRANSFORM NAME> path/of/files/ or/some**/*glob.js.

示例

npx @reduxjs/rtk-codemods createReducerBuilder ./src

npx @reduxjs/rtk-codemods createSliceBuilder ./packages/my-app/**/*.ts

我们还建议在提交更改之前重新在代码库上运行 Prettier。

这些代码修改器应该可以工作,但我们非常感谢来自更多真实世界代码库的反馈!

configureStore.middleware 必须是回调函数

从一开始,configureStore 就接受直接数组值作为 middleware 选项。但是,直接提供数组会阻止 configureStore 调用 getDefaultMiddleware()。因此,middleware: [myMiddleware] 表示没有添加 thunk 中间件(或任何开发模式检查)。

这是一个陷阱,我们已经有很多用户不小心这样做,导致他们的应用程序失败,因为默认中间件从未配置。

因此,我们现在已将 middleware 改为仅接受回调形式。如果出于某种原因您仍然想要替换所有内置中间件,请从回调函数中返回一个数组

const store = configureStore({
reducer,
middleware: getDefaultMiddleware => {
// WARNING: this means that _none_ of the default middleware are added!
return [myMiddleware]
// or for TS users, use:
// return new Tuple(myMiddleware)
}
})

但请注意,我们始终建议不要完全替换默认中间件,并且您应该使用 return getDefaultMiddleware().concat(myMiddleware)

configureStore.enhancers 必须是回调函数

configureStore.middleware 类似,enhancers 字段也必须是回调函数,原因相同。

回调函数将接收一个 getDefaultEnhancers 函数,可用于自定义批处理增强器 现在默认包含

例如

const store = configureStore({
reducer,
enhancers: getDefaultEnhancers => {
return getDefaultEnhancers({
autoBatch: { type: 'tick' }
}).concat(myEnhancer)
}
})

需要注意的是,getDefaultEnhancers 的结果将 **也** 包含使用任何配置/默认中间件创建的中间件增强器。为了帮助防止错误,如果提供了中间件但回调结果中未包含中间件增强器,configureStore 将在控制台中记录错误。

const store = configureStore({
reducer,
enhancers: getDefaultEnhancers => {
return [myEnhancer] // we've lost the middleware here
// instead:
return getDefaultEnhancers().concat(myEnhancer)
}
})

独立的 getDefaultMiddlewaregetType 已移除

getDefaultMiddleware 的独立版本自 v1.6.1 起已弃用,现已移除。请改用传递给 middleware 回调函数的函数,该函数具有正确的类型。

我们还移除了 getType 导出,该导出用于从使用 createAction 创建的操作创建者中提取类型字符串。请改用静态属性 actionCreator.type

RTK Query 行为变更

我们收到了许多关于 RTK Query 在使用 dispatch(endpoint.initiate(arg, {subscription: false})) 时出现问题的报告。还有报告称,多个触发的延迟查询在错误的时间解析了 Promise。这两个问题都存在相同的基础问题,即 RTKQ 在这些情况下(有意地)没有跟踪缓存条目。我们重新设计了逻辑以始终跟踪缓存条目(并在需要时将其删除),这应该可以解决这些行为问题。

我们还收到了关于尝试连续运行多个突变以及标签失效行为的问题。RTKQ 现在具有内部逻辑来短暂延迟标签失效,以允许将多个失效一起处理。这由 createApi 上的新 invalidationBehavior: 'immediate' | 'delayed' 标志控制。新的默认行为是 'delayed'。将其设置为 'immediate' 以恢复到 RTK 1.9 中的行为。

在 RTK 1.9 中,我们重新设计了 RTK Query 的内部机制,将大部分订阅状态保留在 RTKQ 中间件中。这些值仍然与 Redux 存储状态同步,但这主要是为了由 Redux DevTools 的“RTK Query”面板显示。与上面的缓存条目更改相关,我们优化了这些值与 Redux 状态同步的频率,以提高性能。

reactHooksModule 自定义钩子配置

以前,React Redux 钩子的自定义版本(useSelectoruseDispatchuseStore)可以分别传递给 reactHooksModule,通常是为了启用使用与默认 ReactReduxContext 不同的上下文。

实际上,react 钩子模块需要提供所有这三个钩子,并且只传递 useSelectoruseDispatch 而没有 useStore 成为一个容易犯的错误。

该模块现在已将所有这三个钩子移至同一个配置键下,如果存在该键,它将检查是否提供了所有三个钩子。

// previously
const customCreateApi = buildCreateApi(
coreModule(),
reactHooksModule({
useDispatch: createDispatchHook(MyContext),
useSelector: createSelectorHook(MyContext),
useStore: createStoreHook(MyContext)
})
)

// now
const customCreateApi = buildCreateApi(
coreModule(),
reactHooksModule({
hooks: {
useDispatch: createDispatchHook(MyContext),
useSelector: createSelectorHook(MyContext),
useStore: createStoreHook(MyContext)
}
})
)

错误消息提取

Redux 4.1.0 通过 从生产版本中提取错误消息字符串 优化了其捆绑包大小,这基于 React 的方法。我们已将相同的技术应用于 RTK。这从生产捆绑包中节省了大约 1000 字节(实际收益将取决于使用哪些导入)。

configureStore 字段顺序对 middleware 很重要

如果您同时将 middlewareenhancers 字段传递给 configureStore,则 middleware 字段必须排在前面,以便内部 TS 推断能够正常工作。

非默认中间件/增强器必须使用 Tuple

我们已经看到很多用户将 middleware 参数传递给 configureStore 时,尝试展开 getDefaultMiddleware() 返回的数组,或者传递一个备用普通数组。不幸的是,这会丢失来自各个中间件的精确 TS 类型,并且经常会导致 TS 问题(例如 dispatch 被类型化为 Dispatch<AnyAction> 并且不知道关于 thunk 的信息)。

getDefaultMiddleware() 已经使用了内部 MiddlewareArray 类,这是一个 Array 子类,它具有强类型化的 .concat/prepend() 方法,以正确捕获和保留中间件类型。

我们已将该类型重命名为 Tuple,并且 configureStore 的 TS 类型现在要求您必须使用 Tuple,如果您想传递自己的中间件数组。

import { configureStore, Tuple } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => new Tuple(additionalMiddleware, logger)
})

(请注意,如果您使用的是带有纯 JS 的 RTK,这不会产生任何影响,您仍然可以在这里传递一个普通数组。)

相同的限制也适用于 enhancers 字段。

实体适配器类型更新

createEntityAdapter 现在有一个 Id 泛型参数,它将用于在任何暴露这些参数的地方对项目 ID 进行强类型化。以前,ID 字段类型始终为 string | number。TS 现在将尝试从您的实体类型的 .id 字段或 selectId 返回类型推断出确切的类型。您也可以回退到直接传递该泛型类型。如果您直接使用 EntityState<Data, Id> 类型,则必须提供两个泛型参数!

.entities 查找表现在定义为使用标准 TS Record<Id, MyEntityType>,它假设每个项目查找默认情况下都存在。以前,它使用 Dictionary<MyEntityType> 类型,它假设结果为 MyEntityType | undefinedDictionary 类型已被删除。

如果您希望假设查找可能为未定义,请使用 TypeScript 的 noUncheckedIndexedAccess 配置选项来控制这一点。

Reselect

createSelector 使用 weakMapMemoize 作为默认记忆器

createSelector 现在使用一个名为 weakMapMemoize 的新默认记忆函数。此记忆器提供了一个实际上无限的缓存大小,这应该简化了对不同参数的使用,但仅依赖于引用比较。

如果您需要自定义相等性比较,请自定义 createSelector 以使用原始 lruMemoize 方法。

createSelector(inputs, resultFn, {
memoize: lruMemoize,
memoizeOptions: { equalityCheck: yourEqualityFunction }
})

defaultMemoize 重命名为 lruMemoize

由于原始 defaultMemoize 函数不再是实际的默认函数,因此我们将其重命名为 lruMemoize 以提高清晰度。这只有在您专门将其导入到您的应用程序中以自定义选择器时才重要。

createSelector 开发模式检查

createSelector 现在在开发模式下对常见错误进行检查,例如始终返回新引用的输入选择器,或立即返回其参数的结果函数。这些检查可以在选择器创建时或全局进行自定义。

这很重要,因为输入选择器在相同参数下返回的结果存在重大差异,意味着输出选择器将永远无法正确记忆并被不必要地运行,从而(可能)创建新的结果并导致重新渲染。

const addNumbers = createSelector(
// this input selector will always return a new reference when run
// so cache will never be used
(a, b) => ({ a, b }),
({ a, b }) => ({ total: a + b })
)
// instead, you should have an input selector for each stable piece of data
const addNumbersStable = createSelector(
(a, b) => a,
(a, b) => b,
(a, b) => ({
total: a + b
})
)

这是在选择器第一次被调用时完成的,除非另有配置。更多详细信息可以在 Reselect 文档的开发模式检查部分 中找到。

请注意,虽然 RTK 重新导出了 createSelector,但它有意地没有重新导出用于全局配置此检查的函数 - 如果你希望这样做,你应该直接依赖 reselect 并自己导入它。

ParametricSelector 类型已移除

ParametricSelectorOutputParametricSelector 类型已被移除。请使用 SelectorOutputSelector 代替。

React-Redux

需要 React 18

React-Redux v7 和 v8 支持所有支持钩子的 React 版本(16.8+、17 和 18)。v8 从内部订阅管理切换到 React 的新 useSyncExternalStore 钩子,但使用“垫片”实现来提供对 React 16.8 和 17 的支持,这两个版本没有内置该钩子。

React-Redux v9 切换到需要 React 18,并且不支持 React 16 或 17。这使我们能够删除垫片并节省一小部分捆绑包大小。

Redux Thunk

Thunk 使用命名导出

redux-thunk 包以前使用单个默认导出,该导出是中间件,并附带一个名为 withExtraArgument 的字段,允许自定义。

默认导出已被移除。现在有两个命名导出:thunk(基本中间件)和 withExtraArgument

如果你正在使用 Redux Toolkit,这应该不会有任何影响,因为 RTK 已经在 configureStore 中处理了这一点。

新功能

这些功能是 Redux Toolkit 2.0 中的新功能,有助于涵盖我们看到用户在生态系统中要求的更多用例。

combineSlices API 与切片 reducer 注入用于代码分割

Redux 核心一直包含 combineReducers,它接受一个包含“切片 reducer”函数的对象,并生成一个调用这些切片 reducer 的 reducer。RTK 的 createSlice 生成切片 reducer + 相关的 action creators,并且我们已经教导了将单个 action creators 作为命名导出,并将切片 reducer 作为默认导出。同时,我们从未对延迟加载 reducer 提供官方支持,尽管我们在文档中提供了一些“reducer 注入”模式的示例代码。 示例代码.

此版本包含一个新的 combineSlices API,旨在支持在运行时延迟加载 reducer。它接受单个切片或包含切片的对象作为参数,并自动使用 sliceObject.name 字段作为每个状态字段的键调用 combineReducers。生成的 reducer 函数附加了一个额外的 .inject() 方法,可用于在运行时动态注入额外的切片。它还包含一个 .withLazyLoadedSlices() 方法,可用于为稍后添加的 reducer 生成 TS 类型。有关此想法的原始讨论,请参阅 #2776

目前,我们不会将其构建到 configureStore 中,因此您需要自己调用 const rootReducer = combineSlices(.....) 并将其传递给 configureStore({reducer: rootReducer})

基本用法:传递给 combineSlices 的切片和独立 reducer 的混合

const stringSlice = createSlice({
name: 'string',
initialState: '',
reducers: {}
})

const numberSlice = createSlice({
name: 'number',
initialState: 0,
reducers: {}
})

const booleanReducer = createReducer(false, () => {})

const api = createApi(/* */)

const combinedReducer = combineSlices(
stringSlice,
{
num: numberSlice.reducer,
boolean: booleanReducer
},
api
)
expect(combinedReducer(undefined, dummyAction())).toEqual({
string: stringSlice.getInitialState(),
num: numberSlice.getInitialState(),
boolean: booleanReducer.getInitialState(),
api: api.reducer.getInitialState()
})

基本切片 reducer 注入

// Create a reducer with a TS type that knows `numberSlice` will be injected
const combinedReducer =
combineSlices(stringSlice).withLazyLoadedSlices<
WithSlice<typeof numberSlice>
>()

// `state.number` doesn't exist initially
expect(combinedReducer(undefined, dummyAction()).number).toBe(undefined)

// Create a version of the reducer with `numberSlice` injected (mainly useful for types)
const injectedReducer = combinedReducer.inject(numberSlice)

// `state.number` now exists, and injectedReducer's type no longer marks it as optional
expect(injectedReducer(undefined, dummyAction()).number).toBe(
numberSlice.getInitialState()
)

// original reducer has also been changed (type is still optional)
expect(combinedReducer(undefined, dummyAction()).number).toBe(
numberSlice.getInitialState()
)

createSlice 中的 selectors 字段

现有的 createSlice API 现在支持直接在切片中定义 selectors。默认情况下,这些选择器将假设切片使用 slice.name 作为字段挂载在根状态中,例如 name: "todos" -> rootState.todos。此外,现在有一个 slice.selectSlice 方法可以执行默认的根状态查找。

您可以调用 sliceObject.getSelectors(selectSliceState) 使用备用位置生成选择器,类似于 entityAdapter.getSelectors() 的工作方式。

const slice = createSlice({
name: 'counter',
initialState: 42,
reducers: {},
selectors: {
selectSlice: state => state,
selectMultiple: (state, multiplier: number) => state * multiplier
}
})

// Basic usage
const testState = {
[slice.name]: slice.getInitialState()
}
const { selectSlice, selectMultiple } = slice.selectors
expect(selectSlice(testState)).toBe(slice.getInitialState())
expect(selectMultiple(testState, 2)).toBe(slice.getInitialState() * 2)

// Usage with the slice reducer mounted under a different key
const customState = {
number: slice.getInitialState()
}
const { selectSlice, selectMultiple } = slice.getSelectors(
(state: typeof customState) => state.number
)
expect(selectSlice(customState)).toBe(slice.getInitialState())
expect(selectMultiple(customState, 2)).toBe(slice.getInitialState() * 2)

createSlice.reducers 回调语法和 thunk 支持

我们收到的最古老的功能请求之一是能够直接在 createSlice 中声明 thunk。到目前为止,您始终需要单独声明它们,为 thunk 提供一个字符串操作前缀,并通过 createSlice.extraReducers 处理操作。

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

const usersSlice = createSlice({
name: 'users',
initialState,
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) => {
state.entities.push(action.payload)
})
}
})

许多用户告诉我们,这种分离感觉很别扭。

我们一直希望包含一种方法来直接在 createSlice 中定义 thunk,并且一直在尝试各种原型。一直存在两个主要阻碍问题,以及一个次要问题。

  1. 不清楚在内部声明 thunk 的语法应该是什么样子。
  2. Thunk 可以访问 getStatedispatch,但 RootStateAppDispatch 类型通常是从 store 推断出来的,而 store 又从切片状态类型推断出来。在 createSlice 中声明 thunk 会导致循环类型推断错误,因为 store 需要切片类型,而切片需要 store 类型。我们不愿意发布一个对我们的 JS 用户来说可以正常工作但对我们的 TS 用户来说却不行的 API,尤其是因为我们希望人们在 RTK 中使用 TS。
  3. 您不能在 ES 模块中执行同步条件导入,并且没有好的方法可以使 createAsyncThunk 导入可选。要么 createSlice 始终依赖它(并将它添加到捆绑包大小中),要么它根本无法使用 createAsyncThunk

我们已经达成以下妥协。

  • 为了使用 `createSlice` 创建异步 thunk,你需要设置一个自定义版本的 `createSlice`,它可以访问 `createAsyncThunk`.
  • 你可以在 `createSlice.reducers` 中声明 thunk,使用类似于 RTK Query 的 `createApi` 中 `build` 回调语法(使用类型化函数创建对象中的字段)的“创建者回调”语法来声明 `reducers` 字段。这样做看起来与 `reducers` 字段的现有“对象”语法略有不同,但仍然非常相似。
  • 你可以自定义 `createSlice` 中 thunk 的一些类型,但你不能自定义 `state` 或 `dispatch` 类型。如果需要这些类型,你可以手动进行 `as` 转换,例如 `getState() as RootState`。

实际上,我们希望这些是合理的权衡。在 `createSlice` 中创建 thunk 已经被广泛要求,所以我们认为这是一个将被使用的 API。如果 TS 自定义选项是一个限制,你仍然可以像往常一样在 `createSlice` 之外声明 thunk,而且大多数异步 thunk 不需要 `dispatch` 或 `getState` - 它们只是获取数据并返回。最后,设置一个自定义的 `createSlice` 允许你选择将 `createAsyncThunk` 包含在你的包大小中(尽管如果直接使用或作为 RTK Query 的一部分,它可能已经被包含 - 在这两种情况下,都没有额外的包大小)。

以下是新的回调语法示例

const createSliceWithThunks = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator }
})

const todosSlice = createSliceWithThunks({
name: 'todos',
initialState: {
loading: false,
todos: [],
error: null
} as TodoState,
reducers: create => ({
// A normal "case reducer", same as always
deleteTodo: create.reducer((state, action: PayloadAction<number>) => {
state.todos.splice(action.payload, 1)
}),
// A case reducer with a "prepare callback" to customize the action
addTodo: create.preparedReducer(
(text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
// action type is inferred from prepare callback
(state, action) => {
state.todos.push(action.payload)
}
),
// An async thunk
fetchTodo: create.asyncThunk(
// Async payload function as the first argument
async (id: string, thunkApi) => {
const res = await fetch(`myApi/todos?id=${id}`)
return (await res.json()) as Item
},
// An object containing `{pending?, rejected?, fulfilled?, settled?, options?}` second
{
pending: state => {
state.loading = true
},
rejected: (state, action) => {
state.error = action.payload ?? action.error
},
fulfilled: (state, action) => {
state.todos.push(action.payload)
},
// settled is called for both rejected and fulfilled actions
settled: (state, action) => {
state.loading = false
}
}
)
})
})

// `addTodo` and `deleteTodo` are normal action creators.
// `fetchTodo` is the async thunk
export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions

Codemod

使用新的回调语法是完全可选的(对象语法仍然是标准的),但现有的切片需要在使用这种语法提供的新的功能之前进行转换。为了使这更容易,提供了一个codemod

npx @reduxjs/rtk-codemods createSliceReducerBuilder ./src/features/todos/slice.ts

"动态中间件" 中间件

Redux 存储的中间件管道在存储创建时是固定的,不能在以后更改。我们已经看到生态系统库试图允许动态添加和删除中间件,这可能对代码拆分等事情有用。

这是一个比较小众的用例,但我们已经构建了我们自己的“动态中间件”中间件版本。在设置时将其添加到 Redux 存储中,它允许您在运行时添加中间件。它还附带一个React hook 集成,它会自动将中间件添加到存储中并返回更新的 dispatch 方法

import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'

const dynamicMiddleware = createDynamicMiddleware()

const store = configureStore({
reducer: {
todos: todosReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(dynamicMiddleware.middleware)
})

// later
dynamicMiddleware.addMiddleware(someOtherMiddleware)

configureStore 默认添加 autoBatchEnhancer

在 v1.9.0 中,我们添加了一个新的 autoBatchEnhancer,它在连续调度多个“低优先级”操作时会短暂延迟通知订阅者。这提高了性能,因为 UI 更新通常是更新过程中最昂贵的环节。RTK Query 默认将大多数内部操作标记为“低优先级”,但您必须将 autoBatchEnhancer 添加到存储中才能从中受益。

我们已经更新了 configureStore,使其默认将 autoBatchEnhancer 添加到存储设置中,这样用户就可以从性能提升中受益,而无需手动调整存储配置。

entityAdapter.getSelectors 接受 createSelector 函数

entityAdapter.getSelectors() 现在接受一个选项对象作为其第二个参数。这允许您传入您自己的首选 createSelector 方法,该方法将用于记忆生成的 selector。如果您想使用 Reselect 的新备用记忆器之一,或者其他具有等效签名的记忆库,这将很有用。

Immer 10.0

Immer 10.0 现在已最终发布,并进行了多项重大改进和更新

  • 更新性能快得多
  • 捆绑包大小更小
  • 更好的 ESM/CJS 包格式
  • 没有默认导出
  • 没有 ES5 回退

我们已经更新了 RTK 以依赖于最终的 Immer 10.0 版本。

Next.js 设置指南

我们现在有一个文档页面,涵盖了如何在 Next.js 中正确设置 Redux。我们已经看到很多关于将 Redux、Next 和 App Router 结合使用的疑问,本指南将提供一些建议。

(目前,Next.js 的 with-redux 示例仍然显示过时的模式 - 我们将很快提交一个 PR 来更新它以匹配我们的文档指南。)

覆盖依赖项

需要一段时间才能让软件包更新其对等依赖项以允许使用 Redux 核心 5.0,在此期间,诸如中间件类型之类的更改会导致感知到的不兼容性。

大多数库可能实际上没有任何与 5.0 不兼容的做法,但由于对 4.0 的对等依赖,它们最终会引入旧的类型声明。

可以通过手动覆盖依赖项解析来解决此问题,npmyarn 都支持此功能。

npm - overrides

NPM 通过 package.json 中的overrides 字段支持此功能。您可以覆盖特定软件包的依赖项,或确保每个引入 Redux 的软件包都接收相同的版本。

单个覆盖 - redux-persist
{
"overrides": {
"redux-persist": {
"redux": "^5.0.0"
}
}
}
全面覆盖
{
"overrides": {
"redux": "^5.0.0"
}
}

yarn - resolutions

Yarn 通过 package.json 中的resolutions 字段支持此功能。与 NPM 一样,您可以覆盖特定软件包的依赖项,或确保每个引入 Redux 的软件包都接收相同的版本。

单个覆盖 - redux-persist
{
"resolutions": {
"redux-persist/redux": "^5.0.0"
}
}
全面覆盖
{
"resolutions": {
"redux": "^5.0.0"
}
}

建议

基于 2.0 和之前版本的变化,有一些思维上的转变值得了解,即使它们并非必不可少。

actionCreator.toString() 的替代方案

作为 RTK 原 API 的一部分,使用 createAction 创建的动作创建者具有自定义的 toString() 覆盖,它返回动作类型。

这主要对 createReducer 的(现已删除)对象语法有用。

const todoAdded = createAction<Todo>('todos/todoAdded')

createReducer(initialState, {
[todoAdded]: (state, action) => {} // toString called here, 'todos/todoAdded'
})

虽然这很方便(Redux 生态系统中的其他库,如 redux-sagaredux-observable 也以各种方式支持它),但它与 Typescript 不兼容,并且总体上有点过于“神奇”。

const test = todoAdded.toString()
// ^? typed as string, rather than specific action type

随着时间的推移,动作创建者也获得了静态的type属性和match方法,它们更加明确,并且与Typescript配合得更好。

const test = todoAdded.type
// ^? 'todos/todoAdded'

// acts as a type predicate
if (todoAdded.match(unknownAction)) {
unknownAction.payload
// ^? now typed as PayloadAction<Todo>
}

为了兼容性,此覆盖仍然存在,但我们鼓励考虑使用任一静态属性来获得更易理解的代码。

例如,使用redux-observable

// before (works in runtime, will not filter types properly)
const epic = (action$: Observable<Action>) =>
action$.pipe(
ofType(todoAdded),
map(action => action)
// ^? still Action<any>
)

// consider (better type filtering)
const epic = (action$: Observable<Action>) =>
action$.pipe(
filter(todoAdded.match),
map(action => action)
// ^? now PayloadAction<Todo>
)

使用redux-saga

// before (still works)
yield takeEvery(todoAdded, saga)

// consider
yield takeEvery(todoAdded.match, saga)
// or
yield takeEvery(todoAdded.type, saga)

未来计划

自定义切片 reducer 创建者

随着createSlice 的回调语法的添加,提出了建议来启用自定义切片 reducer 创建者。这些创建者将能够

  • 通过添加 case 或 matcher reducer 来修改 reducer 行为
  • 将动作(或任何其他有用的函数)附加到slice.actions
  • 将提供的 case reducer 附加到slice.caseReducers

创建者需要首先在第一次调用createSlice时返回一个“定义”形状,然后它通过添加任何必要的 reducer 和/或动作来处理它。

此 API 尚未确定,但使用潜在 API 实现的现有create.asyncThunk创建者可能看起来像

const asyncThunkCreator = {
type: ReducerType.asyncThunk,
define(payloadCreator, config) {
return {
type: ReducerType.asyncThunk, // needs to match reducer type, so correct handler can be called
payloadCreator,
...config
}
},
handle(
{
// the key the reducer was defined under
reducerName,
// the autogenerated action type, i.e. `${slice.name}/${reducerName}`
type
},
// the definition from define()
definition,
// methods to modify slice
context
) {
const { payloadCreator, options, pending, fulfilled, rejected, settled } =
definition
const asyncThunk = createAsyncThunk(type, payloadCreator, options)

if (pending) context.addCase(asyncThunk.pending, pending)
if (fulfilled) context.addCase(asyncThunk.fulfilled, fulfilled)
if (rejected) context.addCase(asyncThunk.rejected, rejected)
if (settled) context.addMatcher(asyncThunk.settled, settled)

context.exposeAction(reducerName, asyncThunk)
context.exposeCaseReducer(reducerName, {
pending: pending || noop,
fulfilled: fulfilled || noop,
rejected: rejected || noop,
settled: settled || noop
})
}
}

const createSlice = buildCreateSlice({
creators: {
asyncThunk: asyncThunkCreator
}
})

我们不确定有多少人/库会真正使用它,因此欢迎您在Github 问题上提供任何反馈!

createSlice.selector 选择器工厂

内部有一些关于createSlice.selectors 是否充分支持记忆选择器的担忧。您可以向createSlice.selectors 配置提供一个记忆选择器,但您只能使用该实例。

const todoSlice = createSlice({
name: 'todos',
initialState: {
todos: [] as Todo[]
},
reducers: {},
selectors: {
selectTodosByAuthor = createSelector(
(state: TodoState) => state.todos,
(state: TodoState, author: string) => author,
(todos, author) => todos.filter(todo => todo.author === author)
)
}
})

export const { selectTodosByAuthor } = todoSlice.selectors

使用createSelector 的默认缓存大小为 1,如果在多个组件中使用不同的参数调用它,这会导致缓存问题。一个典型的解决方案(没有createSlice)是选择器工厂

export const makeSelectTodosByAuthor = () =>
createSelector(
(state: RootState) => state.todos.todos,
(state: RootState, author: string) => author,
(todos, author) => todos.filter(todo => todo.author === author)
)

function AuthorTodos({ author }: { author: string }) {
const selectTodosByAuthor = useMemo(makeSelectTodosByAuthor, [])
const todos = useSelector(state => selectTodosByAuthor(state, author))
}

当然,使用createSlice.selectors,这将不再可能,因为您需要在创建切片时使用选择器实例。

在 2.0.0 中,我们还没有针对此问题的解决方案 - 一些 API 已经提出(PR 1PR 2),但尚未做出决定。如果您希望看到此功能得到支持,请考虑在 Github 讨论 中提供反馈!

3.0 - RTK Query

RTK 2.0 主要集中在核心和工具包的更改上。现在 2.0 已经发布,我们希望将重点转移到 RTK Query,因为仍然有一些需要解决的粗糙边缘 - 其中一些可能需要重大更改,需要 3.0 版本。

如果您对 RTK Query API 的痛点和粗糙边缘反馈线程有任何反馈,请考虑在 RTK Query API 痛点和粗糙边缘反馈线程 中发表意见!