Skip to main content

迁移到 RTK 2.0 和 Redux 5.0

¥Migrating to RTK 2.0 and Redux 5.0

你将学到什么
  • Redux Toolkit 2.0、Redux core 5.0、Reselect 5.0 和 Redux Thunk 3.0 中的更改,包括重大更改和新功能

    ¥What's changed in Redux Toolkit 2.0, Redux core 5.0, Reselect 5.0, and Redux Thunk 3.0, including breaking changes and new features

介绍

¥Introduction

Redux Toolkit 自 2019 年推出,如今它已成为编写 Redux 应用的标准方式。我们已经过去 4 年多了,没有发生任何重大变化。现在,RTK 2.0 让我们有机会对封装进行现代化改造,清理已弃用的选项,并收紧一些边缘情况。

¥Redux Toolkit has been available since 2019, and today it's the standard way to write Redux apps. We've gone 4+ years without any breaking changes. Now, RTK 2.0 gives us a chance to modernize the packaging, clean up deprecated options, and tighten up some edge cases.

Redux Toolkit 2.0 附带所有其他 Redux 包的主要版本:Redux 核心 5.0、React-Redux 9.0、Reselect 5.0 和 Redux Thunk 3.0。

¥Redux Toolkit 2.0 is accompanied by major versions of all the other Redux packages: Redux core 5.0, React-Redux 9.0, Reselect 5.0, and Redux Thunk 3.0.

本页列出了每个包中已知的潜在重大更改,以及 Redux Toolkit 2.0 中的新功能。提醒一下,你不需要直接实际安装或使用核心 redux 软件包 - RTK 对其进行封装,并重新导出所有方法和类型。

¥This page lists known potentially breaking changes in each of those packages, as well as new features in Redux Toolkit 2.0. As a reminder, you should not need to actually install or use the core redux package directly - RTK wraps that, and re-exports all methods and types.

在实践中,大多数 "breaking" 更改不应该对终端用户产生实际影响,我们预计许多项目只需更新软件包版本,只需很少的代码更改。

¥In practice, most of the "breaking" changes should not have an actual effect on end users, and we expect that many projects can just update the package versions with very few code changes needed.

最可能需要应用代码更新的更改是:

¥The changes most likely to need app code updates are:

封装变更(全部)

¥Packaging Changes (all)

我们更新了所有 Redux 相关库的构建包。这些在技术上是 "breaking",但应该对终端用户透明,并且实际上可以更好地支持诸如通过 Node.js 下的 ESM 文件使用 Redux 等场景。

¥We've made updates to the build packaging for all of the Redux-related libraries. These are technically "breaking", but should be transparent to end users, and actually enable better support for scenarios such as using Redux via ESM files under Node.

package.json 中添加 exports 字段

¥Addition of exports field in package.json

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

¥We've migrated the package definitions to include the exports field for defining which artifacts to load, with a modern ESM build as the primary artifact (with CJS still included for compatibility purposes).

我们已经对该包进行了本地测试,但我们要求社区在你自己的项目中尝试这一点并报告你发现的任何损坏!

¥We've done local testing of the package, but we ask the community to try out this in your own projects and report any breakages you find!

构建工件现代化

¥Build Artifact Modernization

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

¥We've updated the build output in several ways:

  • 构建输出不再被转译!相反,我们的目标是现代 JS 语法 (ES2020)

    ¥Build output is no longer transpiled! Instead we target modern JS syntax (ES2020)

  • 将所有构建工件移至 ./dist/ 下,而不是单独的顶层文件夹

    ¥Moved all build artifacts to live under ./dist/, instead of separate top-level folders

  • 我们测试的最低 Typescript 版本现在是 TS 4.7。

    ¥The lowest Typescript version we test against is now TS 4.7.

放弃 UMD 构建

¥Dropping UMD builds

Redux 始终附带 UMD 构建工件。这些主要用于作为脚本标签直接导入,例如在 CodePen 或无打包程序构建环境中。

¥Redux has always shipped with UMD build artifacts. These are primarily meant for direct import as script tags, such as in a CodePen or a no-bundler build environment.

目前,我们正在从已发布的包中删除这些构建工件,因为这些用例今天看起来相当罕见。

¥For now, we're dropping those build artifacts from the published package, on the grounds that the use cases seem pretty rare today.

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

¥We do have a browser-ready ESM build artifact included at dist/$PACKAGE_NAME.browser.mjs, which can be loaded via a script tag that points to that file on Unpkg.

如果你有强有力的用例让我们继续包含 UMD 构建工件,请告诉我们!

¥If you have strong use cases for us continuing to include UMD build artifacts, please let us know!

重大变化

¥Breaking Changes

¥Core

操作类型必须是字符串

¥Action types must be strings

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

¥We've always specifically told our users that actions and state must be serializable, and that action.type should be a string. This is both to ensure that actions are serializable, and to help provide a readable action history in the Redux DevTools.

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

¥store.dispatch(action) now specifically enforces that action.type must be a string and will throw an error if not, in the same way it throws an error if the action is not a plain object.

实际上,99.99% 的情况下这已经是正确的,并且不应该对用户(尤其是使用 Redux Toolkit 和 createSlice 的用户)产生任何影响,但可能有一些旧版 Redux 代码库选择使用 Symbols 作为操作类型。

¥In practice, this was already true 99.99% of the time and shouldn't have any effect on users (especially those using Redux Toolkit and createSlice), but there may be some legacy Redux codebases that opted to use Symbols as action types.

createStore 弃用

¥createStore Deprecation

Redux 4.2.0,我们将原来的 createStore 方法标记为 @deprecated 年。严格来说,这不是一个重大更改,也不是 5.0 中的新内容,但为了完整起见,我们在这里记录它。

¥In Redux 4.2.0, we marked the original createStore method as @deprecated. Strictly speaking, this is not a breaking change, nor is it new in 5.0, but we're documenting it here for completeness.

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

¥This deprecation is solely a visual indicator that is meant to encourage users to migrate their apps from legacy Redux patterns to use the modern Redux Toolkit APIs.

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

¥The deprecation results in a visual strikethrough when imported and used, like createStore, but with no runtime errors or warnings.

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

¥createStore will continue to work indefinitely, and will not ever be removed. But, today we want all Redux users to be using Redux Toolkit for all of their Redux logic.

要解决此问题,有以下三个选项:

¥To fix this, there are three options:

  • 按照我们的强烈建议切换到 Redux Toolkit 和 configureStore

    ¥Follow our strong suggestion to switch over to Redux Toolkit and configureStore

  • 没做什么。这只是一个视觉删除线,它不会影响代码的行为方式。忽略它。

    ¥Do nothing. It's just a visual strikethrough, and it doesn't affect how your code behaves. Ignore it.

  • 切换到使用现在导出的 legacy_createStore API,这是完全相同的函数,但没有 @deprecated 标签。最简单的选择是执行别名导入重命名,例如 import { legacy_createStore as createStore } from 'redux'

    ¥Switch to using the legacy_createStore API that is now exported, which is the exact same function but with no @deprecated tag. The simplest option is to do an aliased import rename, like import { legacy_createStore as createStore } from 'redux'

TypeScript 重写

¥Typescript rewrite

2019 年,我们开始由社区支持将 Redux 代码库转换为 TypeScript。最初的工作在 #3500:移植到 TypeScript 中进行了讨论,该工作已整合到 PR #3536:转换为 TypeScript 中。

¥In 2019, we began a community-powered conversion of the Redux codebase to TypeScript. The original effort was discussed in #3500: Port to TypeScript, and the work was integrated in PR #3536: Convert to TypeScript.

然而,由于担心与现有生态系统可能存在兼容性问题(以及我们的普遍惯性),TS 转换的代码在存储库中放置了几年,未使用且未发布。

¥However, the TS-converted code sat around in the repo for several years, unused and unpublished, due to concerns about possible compatibility issues with the existing ecosystem (as well as general inertia on our part).

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

¥Redux core v5 is now built from that TS-converted source code. In theory, this should be almost identical in both runtime behavior and types to the 4.x build, but it's very likely that some of the changes may cause types issues.

请报告 Github 上任何意外的兼容性问题!

¥Please report any unexpected compatibility issues on Github!

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

¥AnyAction deprecated in favour of UnknownAction

Redux TS 类型始终导出 AnyAction 类型,该类型被定义为具有 {type: string} 并将任何其他字段视为 any。这使得编写像 console.log(action.whatever) 这样的用途变得很容易,但不幸的是没有提供任何有意义的类型安全。

¥The Redux TS types have always exported an AnyAction type, which is defined to have {type: string} and treat any other field as any. This makes it easy to write uses like console.log(action.whatever), but unfortunately does not provide any meaningful type safety.

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

¥We now export an UnknownAction type, which treats all fields other than action.type as unknown. This encourages users to write type guards that check the action object and assert its specific TS type. Inside of those checks, you can access a field with better type safety.

UnknownAction 现在是 Redux 源中任何需要操作对象的默认位置。

¥UnknownAction is now the default any place in the Redux source that expects an action object.

为了兼容,AnyAction 仍然存在,但已被标记为已弃用。

¥AnyAction still exists for compatibility, but has been marked as deprecated.

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

¥Note that Redux Toolkit's action creators have a .match() method that acts as a useful type guard:

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

你还可以使用新的 isAction util 来检查未知值是否是某种操作对象。

¥You can also use the new isAction util to check if an unknown value is some kind of action object.

Middleware 类型变更 - 中间件 actionnext 的类型为 unknown

¥Middleware type changed - Middleware action and next are typed as unknown

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

¥Previously, the next parameter is typed as the D type parameter passed, and action is typed as the Action extracted from the dispatch type. Neither of these are a safe assumption:

  • next 将被键入以包含所有调度扩展,包括链中较早的不再适用的扩展。

    ¥next would be typed to have all of the dispatch extensions, including the ones earlier in the chain that would no longer apply.

    • 从技术上讲,输入 next 作为基本 redux 存储实现的默认 Dispatch 是最安全的,但这会导致 next(action) 错误(因为我们不能保证 action 实际上是 Action) - 并且它不会考虑任何后续中间件,这些中间件除了在看到特定操作时所给出的操作之外返回任何内容。

      ¥Technically it would be mostly safe to type next as the default Dispatch implemented by the base redux store, however this would cause next(action) to error (as we cannot promise action is actually an Action) - and it wouldn't account for any following middlewares that return anything other than the action they're given when they see a specific action.

  • action 不一定是已知的动作,它可以是任何东西 - 例如 thunk 是一个没有 .type 属性的函数(因此 AnyAction 是不准确的)

    ¥action is not necessarily a known action, it can be literally anything - for example a thunk would be a function with no .type property (so AnyAction would be inaccurate)

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

¥We've changed next to be (action: unknown) => unknown (which is accurate, we have no idea what next expects or will return), and changed the action parameter to be unknown (which as above, is accurate).

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

¥In order to safely interact with values or access fields inside of the action argument, you must first do a type guard check to narrow the type, such as isAction(action) or someActionCreator.match(action).

这个新类型与 v4 Middleware 类型不兼容,因此如果包的中间件说它不兼容,请检查它从哪个版本的 Redux 获取类型!(请参阅本页后面的 重写依赖。)

¥This new type is incompatible with the v4 Middleware type, so if a package's middleware is saying it's incompatible, check which version of Redux it's getting its types from! (See overriding dependencies later in this page.)

删除 PreloadedState 类型以支持 Reducer 通用型

¥PreloadedState type removed in favour of Reducer generic

我们对 TS 类型进行了调整,以提高类型安全性和行为。

¥We've made tweaks to the TS types to improve type safety and behavior.

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

¥First, the Reducer type now has a PreloadedState possible generic:

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

根据 #4491 中的解释:

¥Per the explanation in #4491:

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

¥Why the need for this change? When the store is first created by createStore/configureStore, the initial state is set to whatever is passed as the preloadedState argument (or undefined if nothing is passed). That means that the first time that the reducer is called, it is called with the preloadedState. After the first call, the reducer is always passed the current state (which is S).

对于大多数普通的 reducer,S | undefined 准确地描述了 preloadedState 可以传入的内容。然而,combineReducers 功能允许 Partial<S> | undefined 的预加载状态。

¥For most normal reducers, S | undefined accurately describes what can be passed in for the preloadedState. However the combineReducers function allows for a preloaded state of Partial<S> | undefined.

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

¥The solution is to have a separate generic that represents what the reducer accepts for its preloaded state. That way createStore can then use that generic for its preloadedState argument.

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

¥Previously, this was handled by a $CombinedState type, but that complicated things and led to some user-reported issues. This removes the need for $CombinedState altogether.

此更改确实包括一些重大更改,但总体而言不会对用户域中的用户升级产生巨大影响:

¥This change does include some breaking changes, but overall should not have a huge impact on users upgrading in user-land:

  • ReducerReducersMapObjectcreateStore/configureStore 类型/功能采用额外的 PreloadedState 通用,默认为 S

    ¥The Reducer, ReducersMapObject, and createStore/configureStore types/function take an additional PreloadedState generic which defaults to S.

  • 删除了 combineReducers 的重载,取而代之的是采用 ReducersMapObject 作为其泛型参数的单个函数定义。对于这些更改,删除重载是必要的,因为有时会选择错误的重载。

    ¥The overloads for combineReducers are removed in favor of a single function definition that takes the ReducersMapObject as its generic parameter. Removing the overloads was necessary with these changes, since sometimes it was choosing the wrong overload.

  • 明确列出 reducer 泛型的增强器将需要添加第三个泛型。

    ¥Enhancers that explicitly list the generics for the reducer will need to add the third generic.

仅工具包

¥Toolkit only

删除了 createSlice.extraReducerscreateReducer 的对象语法

¥Object syntax for createSlice.extraReducers and createReducer removed

RTK 的 createReducer API 最初设计为接受动作类型字符串的查找表以进行大小写缩减,如 { "ADD_TODO": (state, action) => {} }。后来我们添加了 "构建器回调" 表单,以便更灵活地添加 "matchers" 和默认处理程序,并对 createSlice.extraReducers 执行了相同的操作。

¥RTK's createReducer API was originally designed to accept a lookup table of action type strings to case reducers, like { "ADD_TODO": (state, action) => {} }. We later added the "builder callback" form to allow more flexibility in adding "matchers" and a default handler, and did the same for createSlice.extraReducers.

我们在 RTK 2.0 中删除了 createReducercreateSlice.extraReducers 的 "object" 表单,因为构建器回调表单实际上具有相同数量的代码行,并且与 TypeScript 配合使用效果更好。

¥We have removed the "object" form for both createReducer and createSlice.extraReducers in RTK 2.0, as the builder callback form is effectively the same number of lines of code, and works much better with TypeScript.

举个例子:

¥As an example, this:

const todoAdded = createAction('todos/todoAdded')

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

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

应该迁移到:

¥should be migrated to:

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

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

¥Codemods

为了简化代码库的升级,我们发布了一组代码模块,可以自动将已弃用的 "object" 语法转换为等效的 "builder" 语法。

¥To simplify upgrading codebases, we've published a set of codemods that will automatically transform the deprecated "object" syntax into the equivalent "builder" syntax.

codemods 包在 NPM 上以 @reduxjs/rtk-codemods 的形式提供。更多详细信息请参见 此处

¥The codemods package is available on NPM as @reduxjs/rtk-codemods. More details are available here.

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

¥To run the codemods against your codebase, run npx @reduxjs/rtk-codemods <TRANSFORM NAME> path/of/files/ or/some**/*glob.js.

示例:

¥Examples:

npx @reduxjs/rtk-codemods createReducerBuilder ./src

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

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

¥We also recommend re-running Prettier on the codebase before committing the changes.

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

¥These codemods should work, but we would greatly appreciate feedback from more real-world codebases!

configureStore.middleware 必须是回调

¥configureStore.middleware must be a callback

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

¥Since the beginning, configureStore has accepted a direct array value as the middleware option. However, providing an array directly prevents configureStore from calling getDefaultMiddleware(). So, middleware: [myMiddleware] means there is no thunk middleware added (or any of the dev-mode checks).

这是一把枪,我们有许多用户不小心这样做并导致他们的应用失败,因为默认中间件从未配置过。

¥This is a footgun, and we've had numerous users accidentally do this and cause their apps to fail because the default middleware never got configured.

结果,我们现在让 middleware 只接受回调形式。如果由于某种原因你仍然想替换所有内置中间件,请通过从回调返回一个数组来实现:

¥As a result, we've now made the middleware only accept the callback form. If for some reason you still want to replace all of the built-in middleware, do so by returning an array from the callback:

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)

¥But note that we consistently recommend not replacing the default middleware entirely, and that you should use return getDefaultMiddleware().concat(myMiddleware).

configureStore.enhancers 必须是回调

¥configureStore.enhancers must be a callback

configureStore.middleware 类似,出于同样的原因,enhancers 字段也必须是回调。

¥Similarly to configureStore.middleware, the enhancers field must also be a callback, for the same reasons.

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

¥The callback will receive a getDefaultEnhancers function that can be used to customise the batching enhancer that's now included by default.

例如:

¥For example:

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

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

¥It's important to note that the result of getDefaultEnhancers will also contain the middleware enhancer created with any configured/default middleware. To help prevent mistakes, configureStore will log an error to console if middleware was provided and the middleware enhancer wasn't included in the callback result.

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

独立 getDefaultMiddlewaregetType 已删除

¥Standalone getDefaultMiddleware and getType removed

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

¥The standalone version of getDefaultMiddleware has been deprecated since v1.6.1, and has now been removed. Use the function passed to the middleware callback instead, which has the correct types.

我们还删除了 getType 导出,该导出用于从使用 createAction 制作的动作创建器中提取类型字符串。相反,请使用静态属性 actionCreator.type

¥We have also removed the getType export, which was used to extract a type string from action creators made with createAction. Instead, use the static property actionCreator.type.

RTK 查询行为更改

¥RTK Query behaviour changes

我们收到许多报告,其中 RTK 查询在 dispatch(endpoint.initiate(arg, {subscription: false})) 的使用方面存在问题。还有报告称,多个触发的惰性查询在错误的时间解决了 promise。这两者都有相同的根本问题,即 RTKQ 在这些情况下没有跟踪缓存条目(故意)。我们重新设计了逻辑以始终跟踪缓存条目(并根据需要删除它们),这应该可以解决这些行为问题。

¥We've had a number of reports where RTK Query had issues around usage of dispatch(endpoint.initiate(arg, {subscription: false})). There were also reports that multiple triggered lazy queries were resolving the promises at the wrong time. Both of these had the same underlying issue, which was that RTKQ wasn't tracking cache entries in these cases (intentionally). We've reworked the logic to always track cache entries (and remove them as needed), which should resolve those behavior issues.

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

¥We also have had issues raised about trying to run multiple mutations in a row and how tag invalidation behaves. RTKQ now has internal logic to delay tag invalidation briefly, to allow multiple invalidations to get handled together. This is controlled by a new invalidationBehavior: 'immediate' | 'delayed' flag on createApi. The new default behavior is 'delayed'. Set it to 'immediate' to revert to the behavior in RTK 1.9.

在 RTK 1.9 中,我们重新设计了 RTK Query 的内部结构,以将大部分订阅状态保留在 RTKQ 中间件内。这些值仍然同步到 Redux 存储状态,但这主要用于 Redux DevTools "RTK 查询" 面板显示。与上面的缓存条目更改相关,我们优化了这些值同步到 Redux 状态的频率以获取性能。

¥In RTK 1.9, we reworked RTK Query's internals to keep most of the subscription status inside the RTKQ middleware. The values are still synced to the Redux store state, but this is primarily for display by the Redux DevTools "RTK Query" panel. Related to the cache entry changes above, we've optimized how often those values get synced to the Redux state for perf.

reactHooksModule 自定义钩子配置

¥reactHooksModule custom hook configuration

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

¥Previously, custom versions of React Redux's hooks (useSelector, useDispatch, and useStore) could be passed separately to reactHooksModule, usually to enable using a different context to the default ReactReduxContext.

实际上,react hooks 模块需要提供所有这三个钩子,并且只传递 useSelectoruseDispatch,而不传递 useStore 就很容易出错。

¥In practicality, the react hooks module needs all three of these hooks to be provided, and it became an easy mistake to only pass useSelector and useDispatch, without useStore.

该模块现在已将所有这三个项移到同一配置密钥下,并且如果该密钥存在,将检查是否提供了所有这三个项。

¥The module has now moved all three of these under the same configuration key, and will check that all three are provided if the key is present.

// 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)
}
})
)

错误信息提取

¥Error message extraction

基于 React 的方法,Redux 4.1.0 将其包大小优化了 从生产版本中提取错误消息字符串。我们已将相同的技术应用于 RTK。这可以从产品包中节省大约 1000 个字节(实际好处将取决于所使用的导入)。

¥Redux 4.1.0 optimized its bundle size by extracting error message strings out of production builds, based on React's approach. We've applied the same technique to RTK. This saves about 1000 bytes from prod bundles (actual benefits will depend on which imports are being used).

configureStore 字段顺序用于 middleware 事项

¥configureStore field order for middleware matters

如果你将 middlewareenhancers 字段都传递给 configureStore,则 middleware 字段必须首先出现,以便内部 TS 推断正常工作。

¥If you are passing both the middleware and enhancers fields to configureStore, the middleware field must come first in order for internal TS inference to work properly.

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

¥Non-default middleware/enhancers must use Tuple

我们见过很多情况,用户将 middleware 参数传递给 configureStore 时尝试扩展 getDefaultMiddleware() 返回的数组,或传递替代的普通数组。不幸的是,这会丢失各个中间件的确切 TS 类型,并且经常会导致 TS 问题(例如 dispatch 被键入为 Dispatch<AnyAction> 并且不知道 thunk)。

¥We've seen many cases where users passing the middleware parameter to configureStore have tried spreading the array returned by getDefaultMiddleware(), or passed an alternate plain array. This unfortunately loses the exact TS types from the individual middleware, and often causes TS problems down the road (such as dispatch being typed as Dispatch<AnyAction> and not knowing about thunks).

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

¥getDefaultMiddleware() already used an internal MiddlewareArray class, an Array subclass that had strongly typed .concat/prepend() methods to correctly capture and retain the middleware types.

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

¥We've renamed that type to Tuple, and configureStore's TS types now require that you must use Tuple if you want to pass your own array of middleware:

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

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

(请注意,如果你将 RTK 与纯 JS 一起使用,则这不会产生任何影响,并且你仍然可以在此处传递纯数组。)

¥(Note that this has no effect if you're using RTK with plain JS, and you could still pass a plain array here.)

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

¥This same restriction applies to the enhancers field.

实体适配器类型更新

¥Entity adapter type updates

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

¥createEntityAdapter now has an Id generic argument, which will be used to strongly type the item IDs anywhere those are exposed. Previously, the ID field type was always string | number. TS will now try to infer the exact type from either the .id field of your entity type, or the selectId return type. You could also fall back to passing that generic type directly. If you use the EntityState<Data, Id> type directly, you must supply both generic arguments!

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

¥The .entities lookup table is now defined to use a standard TS Record<Id, MyEntityType>, which assumes that each item lookup exists by default. Previously, it used a Dictionary<MyEntityType> type, which assumed the result was MyEntityType | undefined. The Dictionary type has been removed.

如果你更愿意假设查找可能未定义,请使用 TypeScript 的 noUncheckedIndexedAccess 配置选项来控制它。

¥If you prefer to assume that the lookups might be undefined, use TypeScript's noUncheckedIndexedAccess configuration option to control that.

重新选择

¥Reselect

createSelector 使用 weakMapMemoize 作为默认存储器

¥createSelector Uses weakMapMemoize As Default Memoizer

createSelector 现在使用名为 weakMapMemoize 的新默认记忆功能。该存储器提供了有效的无限缓存大小,这应该可以简化不同参数的使用,但完全依赖于引用比较。

¥createSelector now uses a new default memoization function called weakMapMemoize. This memoizer offers an effectively infinite cache size, which should simplify usage with varying arguments, but relies exclusively on reference comparisons.

如果需要自定义相等比较,请自定义 createSelector 以使用原来的 lruMemoize 方法:

¥If you need to customize equality comparisons, customize createSelector to use the original lruMemoize method instead:

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

defaultMemoize 更名为 lruMemoize

¥defaultMemoize Renamed to lruMemoize

由于原来的 defaultMemoize 函数实际上不再是默认函数,为了清楚起见,我们将其重命名为 lruMemoize。仅当你专门将其导入应用以自定义选择器时,这才重要。

¥Since the original defaultMemoize function is no longer actually the default, we've renamed it to lruMemoize for clarity. This only matters if you specifically imported it into your app to customize selectors.

createSelector 开发模式检查

¥createSelector Dev-Mode Checks

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

¥createSelector now does checks in development mode for common mistakes, like input selectors that always return new references, or result functions that immediately return their argument. These checks can be customized at selector creation or globally.

这很重要,因为输入选择器使用相同的参数返回实质上不同的结果意味着输出选择器永远不会正确记忆并不必要地运行,从而(可能)创建新结果并导致重新渲染。

¥This is important, as an input selector returning a materially different result with the same parameters means that the output selector will never memoize correctly and be run unnecessarily, thus (potentially) creating a new result and causing rerenders.

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
})
)

除非另有配置,否则这是第一次调用选择器时完成的。更多详细信息请参见 重新选择开发模式检查的文档

¥This is done the first time the selector is called, unless configured otherwise. More details are available in the Reselect docs on dev-mode checks.

请注意,虽然 RTK 重新导出 createSelector,但它故意不重新导出全局配置此检查的功能 - 如果你想这样做,你应该直接依赖 reselect 并自己导入它。

¥Note that while RTK re-exports createSelector, it intentionally does not re-export the function to configure this check globally - if you wish to do so, you should instead depend on reselect directly and import it yourself.

ParametricSelector 删除的类型

¥ParametricSelector Types Removed

ParametricSelectorOutputParametricSelector 类型已被删除。请改用 SelectorOutputSelector

¥The ParametricSelector and OutputParametricSelector types have been removed. Use Selector and OutputSelector instead.

React-Redux

需要 React 18

¥Requires React 18

React-Redux v7 和 v8 适用于支持 hooks 的所有 React 版本(16.8+、17 和 18)。v8 从内部订阅管理切换到 React 新的 useSyncExternalStore 钩子,但使用 "shim" 实现来为 React 16.8 和 17 提供支持,而 React 16.8 和 17 没有内置该钩子。

¥React-Redux v7 and v8 worked with all versions of React that supported hooks (16.8+, 17, and 18). v8 switched from internal subscription management to React's new useSyncExternalStore hook, but used the "shim" implementation to provide support for React 16.8 and 17, which did not have that hook built in.

React-Redux v9 切换为需要 React 18,并且不支持 React 16 或 17。这使我们能够放弃垫片并节省一小部分包的大小。

¥React-Redux v9 switches to requiring React 18, and does not support React 16 or 17. This allows us to drop the shim and save a small bit of bundle size.

Redux Thunk

Thunk 使用命名导出

¥Thunk Uses Named Exports

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

¥The redux-thunk package previously used a single default export that was the middleware, with an attached field named withExtraArgument that allowed customization.

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

¥The default export has been removed. There are now two named exports: thunk (the basic middleware) and withExtraArgument.

如果你使用 Redux Toolkit,这应该不会有任何影响,因为 RTK 已经在 configureStore 内部处理了这个问题。

¥If you are using Redux Toolkit, this should have no effect, as RTK already handles this inside of configureStore.

新功能

¥New Features

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

¥These features are new in Redux Toolkit 2.0, and help cover additional use cases that we've seen users ask for in the ecosystem.

带有用于代码分割的切片 reducer 注入的 combineSlices API

¥combineSlices API with slice reducer injection for code-splitting

Redux 核心始终包含 combineReducers,它采用一个充满 "切片 reducer" 函数的对象并生成一个调用这些切片 reducer 的 reducer。RTK 的 createSlice 生成切片缩减器 + 关联的动作创建器,我们已经教授了将单个动作创建器导出为命名导出并将切片缩减器作为默认导出的模式。与此同时,尽管我们已经有了 我们文档中一些 "reducer 注入" 模式的示例代码,但我们从未获得过对延迟加载 reducer 的官方支持。

¥The Redux core has always included combineReducers, which takes an object full of "slice reducer" functions and generates a reducer that calls those slice reducers. RTK's createSlice generates slice reducers + associated action creators, and we've taught the pattern of exporting individual action creators as named exports and the slice reducer as a default export. Meanwhile, we've never had official support for lazy-loading reducers, although we've had sample code for some "reducer injection" patterns in our docs.

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

¥This release includes a new combineSlices API that is designed to enable lazy-loading of reducers at runtime. It accepts individual slices or an object full of slices as arguments, and automatically calls combineReducers using the sliceObject.name field as the key for each state field. The generated reducer function has an additional .inject() method attached that can be used to dynamically inject additional slices at runtime. It also includes a .withLazyLoadedSlices() method that can be used to generate TS types for reducers that will be added later. See #2776 for the original discussion around this idea.

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

¥For now, we are not building this into configureStore, so you'll need to call const rootReducer = combineSlices(.....) yourself and pass that to configureStore({reducer: rootReducer}).

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

¥Basic usage: a mixture of slices and standalone reducers passed to combineSlices

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 注入

¥Basic slice reducer injection

// 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 字段

¥selectors field in createSlice

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

¥The existing createSlice API now has support for defining selectors directly as part of the slice. By default, these will be generated with the assumption that the slice is mounted in the root state using slice.name as the field, such as name: "todos" -> rootState.todos. Additionally, there's now a slice.selectSlice method that does that default root state lookup.

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

¥You can call sliceObject.getSelectors(selectSliceState) to generate the selectors with an alternate location, similar to how entityAdapter.getSelectors() works.

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.reducers callback syntax and thunk support

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

¥One of the oldest feature requests we've had is the ability to declare thunks directly inside of createSlice. Until now, you've always had to declare them separately, give the thunk a string action prefix, and handle the actions via 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)
})
}
})

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

¥Many users have told us that this separation feels awkward.

我们希望包含一种直接在 createSlice 内部定义 thunk 的方法,并尝试了各种原型。总是存在两个主要的阻塞问题和一个次要的问题:

¥We've wanted to include a way to define thunks directly inside of createSlice, and have played around with various prototypes. There were always two major blocking issues, and a secondary concern:

  1. 目前还不清楚在内部声明 thunk 的语法应该是什么样的。

    ¥It wasn't clear what the syntax for declaring a thunk inside should look like.

  2. Thunk 可以访问 getStatedispatch,但 RootStateAppDispatch 类型通常是从存储中推断出来的,而存储又从切片状态类型中推断出来。在 createSlice 内声明 thunk 会导致循环类型推断错误,因为存储需要切片类型,但切片需要存储类型。我们不愿意提供一个对我们的 JS 用户可以正常工作但对我们的 TS 用户来说不行的 API,特别是因为我们希望人们将 TS 与 RTK 结合使用。

    ¥Thunks have access to getState and dispatch, but the RootState and AppDispatch types are normally inferred from the store, which in turn infers it from the slice state types. Declaring thunks inside createSlice would cause circular type inference errors, as the store needs the slice types but the slice needs the store types. We weren't willing to ship an API that would work okay for our JS users but not for our TS users, especially since we want people to use TS with RTK.

  3. 你无法在 ES 模块中进行同步条件导入,并且没有好方法使 createAsyncThunk 导入成为可选。要么 createSlice 总是依赖于它(并将其添加到包大小中),要么它根本不能使用 createAsyncThunk

    ¥You can't do synchronous conditional imports in ES modules, and there's no good way to make the createAsyncThunk import optional. Either createSlice always depends on it (and adds that to the bundle size), or it can't use createAsyncThunk at all.

我们已经做出了这些妥协:

¥We've settled on these compromises:

  • 为了使用 createSlice 创建异步 thunk,你特别需要 设置可以访问 createAsyncThunkcreateSlice 自定义版本

    ¥In order to create async thunks with createSlice, you specifically need to set up a custom version of createSlice that has access to createAsyncThunk.

  • 你可以在 createSlice.reducers 内部声明 thunk,方法是对 reducers 字段使用 "创建者回调" 语法,该语法类似于 RTK 查询的 createApi 中的 build 回调语法(使用类型化函数在对象中创建字段)。这样做确实看起来与 reducers 字段的现有 "object" 语法有点不同,但仍然非常相似。

    ¥You can declare thunks inside of createSlice.reducers, by using a "creator callback" syntax for the reducers field that is similar to the build callback syntax in RTK Query's createApi (using typed functions to create fields in an object). Doing this does look a bit different than the existing "object" syntax for the reducers field, but is still fairly similar.

  • 你可以自定义 createSlice 内部 thunk 的某些类型,但不能自定义 statedispatch 类型。如果需要这些,你可以手动执行 as 转换,例如 getState() as RootState

    ¥You can customize some of the types for thunks inside of createSlice, but you cannot customize the state or dispatch types. If those are needed, you can manually do an as cast, like getState() as RootState.

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

¥In practice, we hope these are reasonable tradeoffs. Creating thunks inside of createSlice has been widely asked for, so we think it's an API that will see usage. If the TS customization options are a limitation, you can still declare thunks outside of createSlice as always, and most async thunks don't need dispatch or getState - they just fetch data and return. And finally, setting up a custom createSlice allows you to opt into createAsyncThunk being included in your bundle size (though it may already be included if used directly or as part of RTK Query - in either of these cases there's no additional bundle size).

新的回调语法如下:

¥Here's what the new callback syntax looks like:

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

¥Using the new callback syntax is entirely optional (the object syntax is still standard), but an existing slice would need to be converted before it can take advantage of the new capabilities this syntax provides. To make this easier, a codemod is provided.

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

"动态中间件" 中间件

¥"Dynamic middleware" middleware

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

¥A Redux store's middleware pipeline is fixed at store creation time and can't be changed later. We have seen ecosystem libraries that tried to allow dynamically adding and removing middleware, potentially useful for things like code splitting.

这是一个相对小众的用例,但我们已经构建了 我们自己的 "动态中间件" 中间件版本。在设置时将其添加到 Redux 存储,它允许你稍后在运行时添加中间件。它还配备了 React hook 集成将自动向存储添加中间件并返回更新的调度方法。

¥This is a relatively niche use case, but we've built our own version of a "dynamic middleware" middleware. Add it to the Redux store at setup time, and it lets you add middleware later at runtime. It also comes with a React hook integration that will automatically add a middleware to the store and return the updated dispatch method..

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

¥configureStore adds autoBatchEnhancer by default

当连续调度多个 "low-priority" 操作时,在 v1.9.0 中,我们添加了新的 autoBatchEnhancer 会短暂延迟通知订阅者。这可以提高性能,因为 UI 更新通常是更新过程中最昂贵的部分。RTK 查询默认将其大部分内部操作标记为 "low-pri",但你必须将 autoBatchEnhancer 添加到存储中才能从中受益。

¥In v1.9.0, we added a new autoBatchEnhancer that delays notifying subscribers briefly when multiple "low-priority" actions are dispatched in a row. This improves perf, as UI updates are typically the most expensive part of the update process. RTK Query marks most of its own internal actions as "low-pri" by default, but you have to have the autoBatchEnhancer added to the store to benefit from that.

我们更新了 configureStore,默认将 autoBatchEnhancer 添加到存储设置中,以便用户可以从改进的性能中受益,而无需自己手动调整存储配置。

¥We've updated configureStore to add the autoBatchEnhancer to the store setup by default, so that users can benefit from the improved perf without needing to manually tweak the store config themselves.

entityAdapter.getSelectors 接受 createSelector 函数

¥entityAdapter.getSelectors accepts a createSelector function

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

¥entityAdapter.getSelectors() now accepts an options object as its second argument. This allows you to pass in your own preferred createSelector method, which will be used to memoize the generated selectors. This could be useful if you want to use one of Reselect's new alternate memoizers, or some other memoization library with an equivalent signature.

沉浸式 10.0

¥Immer 10.0

沉浸式 10.0 现已最终版本,并有几项重大改进和更新:

¥Immer 10.0 is now final, and has several major improvements and updates:

  • 更新性能更快

    ¥Much faster update perf

  • 打包尺寸更小

    ¥Much smaller bundle size

  • 更好的 ESM/CJS 包格式

    ¥Better ESM/CJS package formatting

  • 无默认导出

    ¥No default export

  • 没有 ES5 后备

    ¥No ES5 fallback

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

¥We've updated RTK to depend on the final Immer 10.0 release.

Next.js 设置指南

¥Next.js Setup Guide

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

¥We now have a docs page that covers how to set up Redux properly with Next.js. We've seen a lot of questions around using Redux, Next, and the App Router together, and this guide should help provide advice.

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

¥(At this time, the Next.js with-redux example is still showing outdated patterns - we're going to file a PR shortly to update that to match our docs guide.)

覆盖依赖

¥Overriding dependencies

软件包需要一段时间才能更新其对等依赖以支持 Redux core 5.0,同时像 中间件类型 这样的更改将导致感知到的不兼容性。

¥It will take a while for packages to update their peer dependencies to allow for Redux core 5.0, and in the meantime changes like the Middleware type will result in perceived incompatibilities.

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

¥It's likely that most libraries will not actually have any practices that are incompatible with 5.0, but due to the peer dependency on 4.0 they end up pulling in old type declarations.

这可以通过手动覆盖依赖解析来解决,npmyarn 都支持该依赖解析。

¥This can be solved by manually overriding the dependency resolution, which is supported by both npm and yarn.

npm - overrides

NPM 通过 package.json 中的 overrides 字段支持这一点。你可以覆盖特定包的依赖,或者确保引入 Redux 的每个包都收到相同的版本。

¥NPM supports this through an overrides field in your package.json. You can override the dependency for a specific package, or make sure that every package that pulls in Redux receives the same version.

Individual override - redux-persist
{
"overrides": {
"redux-persist": {
"redux": "^5.0.0"
}
}
}
Blanket override
{
"overrides": {
"redux": "^5.0.0"
}
}

yarn - resolutions

Yarn 通过 package.json 中的 resolutions 字段支持这一点。就像使用 NPM 一样,你可以覆盖特定包的依赖,或者确保拉入 Redux 的每个包都收到相同的版本。

¥Yarn supports this through a resolutions field in your package.json. Just like with NPM, you can override the dependency for a specific package, or make sure that every package that pulls in Redux receives the same version.

Individual override - redux-persist
{
"resolutions": {
"redux-persist/redux": "^5.0.0"
}
}
Blanket override
{
"resolutions": {
"redux": "^5.0.0"
}
}

建议

¥Recommendations

根据 2.0 和之前版本的变化,思想上发生了一些转变,虽然不是必要的,但值得了解一下。

¥Based on changes in 2.0 and previous versions, there have been some shifts in thinking that are good to know about, if non-essential.

actionCreator.toString() 的替代品

¥Alternatives to actionCreator.toString()

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

¥As part of RTK's original API, action creators made with createAction have a custom toString() override that returns the action type.

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

¥This was primarily useful for the (now removed) object syntax for createReducer:

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

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

虽然这很方便(Redux 生态系统中的其他库,如 redux-sagaredux-observable 已经支持这种功能的各种功能),但它与 Typescript 配合得不好,而且通常有点太 "magic" 了。

¥While this was convenient (and other libraries in the Redux ecosystem such as redux-saga and redux-observable have supported this to various capacities), it didn't play well with Typescript and was generally a bit too "magic".

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

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

¥Over time, the action creator also gained a static type property and match method which were more explicit and worked better with Typescript.

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

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

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

¥For compatibility, this override is still in place, but we encourage considering using either of the static properties for more understandable code.

例如,对于 redux-observable

¥For example, with 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

¥With redux-saga:

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

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

未来的计划

¥Future plans

自定义切片 reducer 创建者

¥Custom slice reducer creators

随着 createSlice 的回调语法 的加入,suggestion 的诞生是为了支持自定义切片 reducer 创建者。这些创作者将能够:

¥With the addition of the callback syntax for createSlice, the suggestion was made to enable custom slice reducer creators. These creators would be able to:

  • 通过添加 case 或 matcher reducer 来修改 reducer 行为

    ¥Modify reducer behaviour by adding case or matcher reducers

  • 将操作(或任何其他有用的功能)附加到 slice.actions

    ¥Attach actions (or any other useful functions) to slice.actions

  • 将提供的外壳 reducer 连接到 slice.caseReducers

    ¥Attach provided case reducers to slice.caseReducers

创建者需要在首次调用 createSlice 时首先返回 "definition" 形状,然后通过添加任何必要的化简器和/或操作来处理该形状。

¥The creator would need to first return a "definition" shape when createSlice is first called, which it then handles by adding any necessary reducers and/or actions.

此 API 并不是一成不变的,但使用潜在 API 实现的现有 create.asyncThunk 创建器可能如下所示:

¥An API for this is not set in stone, but the existing create.asyncThunk creator implemented with a potential API could look like:

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 问题 的反馈!

¥We're not sure how many people/libraries would actually make use of this though, so any feedback over on the Github issue is welcome!

createSlice.selector 选择器工厂

¥createSlice.selector selector factories

内部有人担心 createSlice.selectors 是否充分支持记忆选择器。你可以为 createSlice.selectors 配置提供一个记忆选择器,但你只能使用该实例。

¥There have been some concerns raised internally about whether createSlice.selectors supports memoized selectors sufficiently. You can provide a memoized selector to your createSlice.selectors configuration, but you're stuck with that one instance.

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)是 选择器工厂

¥With createSelector's default cache size of 1, this can cause caching issues if called in multiple components with different arguments. One typical solution for this (without createSlice) is a selector factory:

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,这不再可能,因为你在创建切片时需要选择器实例。

¥Of course, with createSlice.selectors this is no longer possible, as you need the selector instance when creating your slice.

在 2.0.0 中我们对此没有固定的解决方案 - 一些 API 已经浮出水面(PR 1PR 2),但尚未做出任何决定。如果你希望看到支持,请考虑在 Github 讨论 中提供反馈!

¥In 2.0.0 we have no set solution for this - a few APIs have been floated (PR 1, PR 2) but nothing was decided upon. If this is something you'd like to see supported, consider providing feedback in the Github discussion!

3.0 - RTK 查询

¥3.0 - RTK Query

RTK 2.0 主要关注核心和工具包的变化。现在 2.0 已发布,我们希望将重点转移到 RTK 查询,因为仍有一些粗糙的问题需要解决 - 其中一些可能需要重大更改,因此需要发布 3.0 版本。

¥RTK 2.0 was largely focused on core and toolkit changes. Now that 2.0 is released, we would like to shift our focus to RTK Query, as there are still some rough edges to iron out - some of which may require breaking changes, necessitating a 3.0 release.

如果你对此有任何反馈,请考虑在 RTK 查询 API 痛点和难点反馈贴 上发表意见!

¥If you have any feedback for what that could look like, please consider chiming in at the RTK Query API pain points and rough spots feedback thread!