遷移到 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 封裝了該套件,並重新匯出所有方法和類型。
實際上,大部分的「重大」變更不應對最終使用者造成實際影響,我們預期許多專案只要更新套件版本,幾乎不需要變更程式碼。
最有可能需要更新應用程式程式碼的變更如下:
createReducer
和createSlice.extraReducers
已移除物件語法configureStore.middleware
必須是回呼函式Middleware
類型已變更 - Middlewareaction
和next
已設定類型為unknown
封裝變更 (全部)
我們已更新所有 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 action
和 next
被指定為 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
的需求。
此變更確實包含一些重大變更,但整體而言,不應對使用者在使用者端升級造成重大影響
Reducer
、ReducersMapObject
和createStore
/configureStore
類型/函式採用額外的PreloadedState
泛型,其預設為S
。combineReducers
的重載已移除,取而代之的是單一函式定義,將ReducersMapObject
作為其泛型參數。由於有時會選擇錯誤的重載,因此這些變更必須移除重載。- 明確列出 reducer 泛型的增強器需要新增第三個泛型。
僅工具組
createSlice.extraReducers
和 createReducer
的物件語法已移除
RTK 的 createReducer
API 最初設計為接受動作類型字串至案例 reducer 的查詢表,例如 { "ADD_TODO": (state, action) => {} }
。我們稍後新增了「建構器回呼」表單,以允許在新增「比對器」和預設處理常式時有更大的彈性,並對 createSlice.extraReducers
執行相同的操作。
我們已在 RTK 2.0 中移除 createReducer
和 createSlice.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)
},
})
已移除獨立的 getDefaultMiddleware
和 getType
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 的勾子(useSelector
、useDispatch
和 useStore
)的自訂版本可以個別傳遞給 reactHooksModule
,通常用於啟用使用與預設 ReactReduxContext
不同的內容。
實際上,react 勾子模組需要提供這三個勾子,而且很容易犯下只傳遞 useSelector
和 useDispatch
,而不傳遞 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
很重要
如果您將 middleware
和 enhancers
欄位都傳遞給 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 | undefined
。Dictionary
型別已移除。
如果您偏好假設查詢可能未定義,請使用 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
類型
ParametricSelector
和 OutputParametricSelector
類型已移除。請改用 Selector
和 OutputSelector
。
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 使用這個值來確保它們實際上有一個提供的內容)。在「最佳」情況下,會產生類似以下的結果
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
,它們會擲回錯誤,因此不會影響回傳類型。
上述範例現在變成
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,並嘗試了各種原型。一直有兩個主要的阻礙問題,以及一個次要問題
- 不清楚在內部宣告 thunk 的語法應該是什麼樣子。
- Thunk 可以存取
getState
和dispatch
,但RootState
和AppDispatch
類型通常會從儲存體中推斷出來,而儲存體又會從區段狀態類型中推斷出來。在createSlice
中宣告 thunk 會導致循環類型推斷錯誤,因為儲存體需要區段類型,但區段需要儲存體類型。我們不願意發布一個對我們的 JS 使用者來說可以正常運作,但對我們的 TS 使用者來說不行的 API,特別是因為我們希望人們在 RTK 中使用 TS。 - 您無法在 ES 模組中進行同步條件式匯入,而且沒有好的方法可以讓
createAsyncThunk
匯入為選用。createSlice
要嘛總是依賴它(並將其新增到套件大小),要嘛根本無法使用createAsyncThunk
。
我們對這些妥協方案達成共識
- 若要使用
createSlice
建立非同步 thunk,您特別需要設定一個自訂版本的createSlice
,它可以存取createAsyncThunk
. - 您可以在
createSlice.reducers
中宣告 thunk,方法是針對reducers
欄位使用「建立呼叫函數」語法,它類似於 RTK Query 的createApi
中的build
呼叫函數語法(使用已輸入的函數在物件中建立欄位)。這樣做看起來與reducers
欄位的現有「物件」語法有點不同,但仍然相當類似。 - 您可以在
createSlice
中自訂 thunk 的部分類型,但您無法自訂state
或dispatch
類型。如果需要這些類型,您可以手動進行as
轉型,例如getState() as RootState
。
實際上,我們希望這些是合理的取捨。在 createSlice
內建立 thunk 的需求很普遍,因此我們認為這是一個會被使用的 API。如果 TS 自訂選項有其限制,你仍然可以像往常一樣在 createSlice
外部宣告 thunk,而且大多數非同步 thunk 並不需要 dispatch
或 getState
,它們只需要擷取資料並回傳即可。最後,設定自訂 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 的對等相依性,它們最終會引入舊的類型宣告。
這可以用手動覆寫相依性解析來解決,這受到 npm
和 yarn
的支援。
npm
- overrides
NPM 支援這一點,方法是在您的 package.json
中使用 overrides
欄位。您可以覆寫特定套件的相依性,或確保引入 Redux 的每個套件都收到相同的版本。
{
"overrides": {
"redux-persist": {
"redux": "^5.0.0"
}
}
}
{
"overrides": {
"redux": "^5.0.0"
}
}
yarn
- resolutions
Yarn 支援這項功能,透過在 package.json
中加入 resolutions
欄位。就像使用 NPM 一樣,你可以覆寫特定套件的相依性,或確保每個拉取 Redux 的套件都收到相同的版本。
{
"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-saga
和 redux-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 1、PR 2),但尚未決定。如果您希望看到此功能,請考慮在 Github 討論 中提供意見回饋!
3.0 - RTK Query
RTK 2.0 主要專注於核心和工具包變更。現在 2.0 已發布,我們希望將重點轉移到 RTK Query,因為仍有一些粗糙的邊緣需要解決 - 其中一些可能需要重大變更,因此需要發布 3.0。
如果您有任何意見回饋,請考慮在 RTK Query API 痛點和粗糙點回饋串 中提供意見!