跳至主要內容

遷移到 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 必須是字串,否則會擲回錯誤,就像它在動作不是純粹物件時擲回錯誤一樣。

實際上,這在 99.99% 的時間裡已經是事實,不應對使用者(特別是使用 Redux Toolkit 和 createSlice 的使用者)有任何影響,但可能有一些舊版 Redux 程式碼庫選擇使用符號作為動作類型。

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 類型已變更 - Middleware actionnext 被指定為 unknown

先前,next 參數被指定為傳遞的 D 類型參數,而 action 被指定為從 dispatch 類型中萃取的 Action。這些都不是安全的假設

  • next 會被鍵入為具有所有發送擴充功能,包括鏈中較早不再適用的那些功能。
    • 技術上,將 next 鍵入為基本 redux 儲存體實作的預設 Dispatch 大多數情況下是安全的,但這會導致 next(action) 錯誤(因為我們無法保證 action 實際上是 Action) - 而且它不會考量任何後續的 middleware,這些 middleware 在看到特定動作時,會傳回與給予它們的動作不同的任何內容。
  • action 不一定是已知的動作,它可以是任何東西 - 例如 thunk 會是一個沒有 .type 屬性的函式(因此 AnyAction 會不準確)

我們已將 next 變更為 (action: unknown) => unknown(這是準確的,我們不知道 next 預期或將傳回什麼),並將 action 參數變更為 unknown(如上所述,這是準確的)。

為了安全地與 action 參數中的值互動或存取欄位,您必須先進行類型守衛檢查以縮小類型,例如 isAction(action)someActionCreator.match(action)

這個新類型與 v4 Middleware 類型不相容,因此如果套件的 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 最初設計為接受動作類型字串至案例 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 的形式提供。更多詳細資訊可在此處取得 here

若要針對程式碼庫執行程式碼模組,請執行 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。

這些 codemod 應該可以正常運作,但我們非常歡迎來自更多實際程式碼庫的回饋!

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})) 時出現問題。另有報告指出,多個觸發的延遲查詢在錯誤的時間解析承諾。這兩個問題的根本原因相同,即 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 推論正常運作。

非預設的 middleware/enhancers 必須使用 Tuple

我們看過許多案例,使用者在將 middleware 參數傳遞給 configureStore 時,嘗試散佈 getDefaultMiddleware() 傳回的陣列,或傳遞另一個純陣列。不幸的是,這會遺失個別 middleware 的確切 TS 型別,並經常在後續造成 TS 問題(例如 dispatch 被設定為 Dispatch<AnyAction> 型別,而不知道 thunk)。

getDefaultMiddleware() 已使用內部 MiddlewareArray 類別,這是一個 Array 子類別,具有強型別的 .concat/prepend() 方法,可正確擷取並保留 middleware 型別。

我們已將該型別重新命名為 Tuple,而 configureStore 的 TS 型別現在要求您必須使用 Tuple,如果您要傳遞自己的 middleware 陣列

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

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

(請注意,如果您使用純 JS 搭配 RTK,這不會產生任何影響,而且您仍然可以在這裡傳遞純陣列。)

相同的限制也適用於 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 可與支援 hooks 的所有 React 版本搭配使用 (16.8 以上、17 和 18)。v8 從內部訂閱管理切換至 React 的新 useSyncExternalStore hook,但使用「shim」實作來提供對 React 16.8 和 17 的支援,因為這些版本沒有內建該 hook。

React-Redux v9 切換為需要 React 18,並且支援 React 16 或 17。這讓我們可以移除 shim 並節省一點點的套件大小。

自訂內容打字

React Redux 支援使用 自訂內容 來建立 hooks (和 connect),但打字相當不標準。v9 之前的類型需要 Context<ReactReduxContextValue>,但內容預設值通常會以 null 初始化 (因為 hooks 使用這個值來確保它們實際上有一個提供的內容)。在「最佳」情況下,會產生類似以下的結果

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,而 hooks 知道如果收到 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),以使用替代位置產生 selector,類似於 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,特別是因為我們希望人們在 RTK 中使用 TS。
  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

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,當多個「低優先順序」動作連續發送時,它會暫時延後通知訂閱者。這會改善效能,因為使用者介面更新通常是更新程序中最昂貴的部分。RTK Query 預設將其大部分的內部動作標記為「低優先順序」,但你必須將 autoBatchEnhancer 新增至儲存體才能從中受益。

我們已更新 configureStore,以預設將 autoBatchEnhancer 新增至儲存體設定,讓使用者可以受益於改善的效能,而無需手動調整儲存體設定。

entityAdapter.getSelectors 接受 createSelector 函式

entityAdapter.getSelectors() 現在接受一個選項物件作為其第二個參數。這允許您傳入您自己偏好的 createSelector 方法,它將用於記憶化產生的選擇器。如果您想使用 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 建立器。這些建立器將能夠

  • 透過新增案例或比對器 reducer 來修改 reducer 行為
  • 將動作(或任何其他有用的函式)附加到 slice.actions
  • 將提供的案例 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 Query

RTK 2.0 主要專注於核心和工具包變更。現在 2.0 已發布,我們希望將重點轉移到 RTK Query,因為仍有一些粗糙的邊緣需要解決 - 其中一些可能需要重大變更,因此需要發布 3.0。

如果您有任何意見回饋,請考慮在 RTK Query API 痛點和粗糙點回饋串 中提供意見!