跳至主要内容

迁移到 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 文件使用 Redux 等场景。

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 必须 为字符串**,如果不是,则会抛出错误,与 action 不是普通对象时抛出错误的方式相同。

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

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 核心 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 存储实现的默认调度是几乎安全的,但是这会导致 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 首次创建 store 时,初始状态将设置为作为 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) => {})
},
})
Codemods

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

codemods 包可在 NPM 上获取,名为 @reduxjs/rtk-codemods。更多详细信息请访问 此处

要对您的代码库运行 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 对代码库进行格式化。

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

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 已移除

从 v1.6.1 版本开始,getDefaultMiddleware 的独立版本已弃用,现已移除。请改用传递给 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),
})

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

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

实体适配器类型更新

createEntityAdapter 现在有一个 Id 泛型参数,它将用于在任何暴露这些 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。这使我们能够删除垫片并节省一小部分捆绑包大小。

自定义上下文类型

React Redux 支持使用 自定义上下文 创建 hooks(和 connect),但对它的类型化一直不太标准。v9 之前的类型需要 Context<ReactReduxContextValue>,但上下文默认值通常使用 null 初始化(因为钩子使用它来确保它们确实有一个提供的上下文)。在“最佳”情况下,这将导致类似于以下内容的结果

v9 之前的自定义上下文
import { createContext } from 'react'
import {
ReactReduxContextValue,
createDispatchHook,
createSelectorHook,
createStoreHook,
} from 'react-redux'
import { AppStore, RootState, AppDispatch } from './store'

const context = createContext<ReactReduxContextValue>(null as any)

export const useStore = createStoreHook(context).withTypes<AppStore>()
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
export const useSelector = createSelectorHook(context).withTypes<RootState>()

在 v9 中,类型现在与运行时行为匹配。上下文被类型化为保存 ReactReduxContextValue | null,并且钩子知道如果它们收到 null,它们将抛出错误,因此它不会影响返回类型。

上面的例子现在变成

v9+ 自定义上下文
import { createContext } from 'react'
import {
ReactReduxContextValue,
createDispatchHook,
createSelectorHook,
createStoreHook,
} from 'react-redux'
import { AppStore, RootState, AppDispatch } from './store'

const context = createContext<ReactReduxContextValue | null>(null)

export const useStore = createStoreHook(context).withTypes<AppStore>()
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
export const useSelector = createSelectorHook(context).withTypes<RootState>()

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 + 相关操作创建者,我们已经教导了将单个操作创建者导出为命名导出,并将切片 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 类型通常是从存储中推断出来的,而存储又从切片状态类型中推断出来。在 createSlice 中声明 thunk 会导致循环类型推断错误,因为存储需要切片类型,而切片需要存储类型。我们不愿意发布一个对我们的 JS 用户可以正常工作但对我们的 TS 用户却不行的 API,尤其是在我们希望人们将 TS 与 RTK 一起使用的情况下。
  3. 您不能在 ES 模块中执行同步条件导入,并且没有好的方法来使 createAsyncThunk 导入可选。要么 createSlice 始终依赖它(并将它添加到捆绑包大小中),要么它根本无法使用 createAsyncThunk

我们已经达成以下妥协。

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

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

以下是新的回调语法的样子

const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
})

const todosSlice = createAppSlice({
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

代码重构

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

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 方法,该方法将用于记忆生成的 selectors。如果您想使用 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 的最终方案,但现有的 create.asyncThunk 创建器可以实现一个潜在的 API,如下所示

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 查询

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

如果您对 RTK 查询 API 的痛点和粗糙边缘有任何反馈,请考虑在 RTK 查询 API 痛点和粗糙边缘反馈主题 中进行讨论!