遷移至 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 邏輯中使用 Redux Toolkit。
要解決這個問題,有三個選項
- 強烈建議您切換到 Redux Toolkit 和
configureStore
- 什麼都不做。這只是一個視覺上的刪除線,不會影響您的程式碼行為。請忽略它。
- 切換到現在已匯出的
legacy_createStore
API,它與沒有@deprecated
標籤的函式完全相同。最簡單的選項是進行別名匯入重新命名,例如import { legacy_createStore as createStore } from 'redux'
Typescript 重寫
2019 年,我們開始由社群推動的 Redux 程式碼庫轉換到 TypeScript。最初的努力在 #3500:移植到 TypeScript 中討論,而工作在公關 #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
會被設定為具備所有 dispatch 擴充功能,包括鏈中較早不再適用的擴充功能。- 技術上,將
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) => {})
}
})
Codemod
為了簡化升級程式碼庫,我們已發布一組 Codemod,它會自動將已棄用的「物件」語法轉換為等效的「建構器」語法。
Codemod 套件在 NPM 上可用作 @reduxjs/rtk-codemods
。更多詳細資訊可在此處取得 here。
若要針對程式碼庫執行 Codemod,請執行 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 推論正常運作。
非預設中介軟體/強化器必須使用 Tuple
我們已看到許多案例,其中傳遞 middleware
參數給 configureStore的使用者嘗試散佈 getDefaultMiddleware()
傳回的陣列,或傳遞替代的純陣列。很不幸地,這會遺失個別中介軟體的精確 TS 類型,而且經常會在後續造成 TS 問題 (例如 dispatch
被設定為 Dispatch<AnyAction>
類型,而不知道 thunk)。
getDefaultMiddleware()
已使用內部 MiddlewareArray
類別,這是 Array
子類別,具有強類型 .concat/prepend()
方法,可以正確擷取並保留中介軟體類型。
我們已將該類型重新命名為 Tuple
,而 configureStore
的 TS 類型現在要求您必須使用 Tuple
,如果您想要傳遞自己的中介軟體陣列
import { configureStore, Tuple } from '@reduxjs/toolkit'
configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => new Tuple(additionalMiddleware, logger)
})
(請注意,如果您使用純 JS 搭配 RTK,則這不會產生任何影響,而且您仍然可以傳遞純陣列。)
相同的限制也適用於 enhancers
欄位。
實體轉接器類型更新
createEntityAdapter
現在有一個 Id
泛型參數,它將用於強烈類型化項目 ID,這些 ID 可以在任何地方公開。先前,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 可與所有支援掛勾的 React 版本搭配使用(16.8+、17 和 18)。v8 從內部訂閱管理切換到 React 的新 useSyncExternalStore
掛勾,但使用「shim」實作來提供對 React 16.8 和 17 的支援,而這兩個版本沒有內建該掛勾。
React-Redux v9 切換為需要 React 18,且不支援 React 16 或 17。這讓我們可以放棄 shim 並節省一點套件大小。
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
直接定義為區塊的一部分。預設情況下,這些 selector 會假設區塊使用 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,該語法類似於 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 createSliceWithThunks = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator }
})
const todosSlice = createSliceWithThunks({
name: 'todos',
initialState: {
loading: false,
todos: [],
error: null
} as TodoState,
reducers: create => ({
// A normal "case reducer", same as always
deleteTodo: create.reducer((state, action: PayloadAction<number>) => {
state.todos.splice(action.payload, 1)
}),
// A case reducer with a "prepare callback" to customize the action
addTodo: create.preparedReducer(
(text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
// action type is inferred from prepare callback
(state, action) => {
state.todos.push(action.payload)
}
),
// An async thunk
fetchTodo: create.asyncThunk(
// Async payload function as the first argument
async (id: string, thunkApi) => {
const res = await fetch(`myApi/todos?id=${id}`)
return (await res.json()) as Item
},
// An object containing `{pending?, rejected?, fulfilled?, settled?, options?}` second
{
pending: state => {
state.loading = true
},
rejected: (state, action) => {
state.error = action.payload ?? action.error
},
fulfilled: (state, action) => {
state.todos.push(action.payload)
},
// settled is called for both rejected and fulfilled actions
settled: (state, action) => {
state.loading = false
}
}
)
})
})
// `addTodo` and `deleteTodo` are normal action creators.
// `fetchTodo` is the async thunk
export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions
Codemod
使用新的回呼語法完全是可選的(物件語法仍為標準),但現有的區塊需要轉換,才能利用此語法提供的新的功能。為簡化此程序,提供了一個 codemod。
npx @reduxjs/rtk-codemods createSliceReducerBuilder ./src/features/todos/slice.ts
「動態中介軟體」中介軟體
Redux 儲存空間的中介軟體管線在建立儲存空間時固定,且無法在稍後變更。我們已經看過生態系統函式庫嘗試允許動態新增和移除中介軟體,這可能對程式碼分割等事項很有用。
這是一個相對小眾的用例,但我們已經建立了 我們自己的「動態中介軟體」中介軟體版本。在設定時將它新增到 Redux 儲存空間,它讓您可以在稍後的執行階段新增中介軟體。它還附帶一個 React 掛勾整合,它會自動將中介軟體新增到儲存空間並回傳已更新的 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 設定指南
我們現在有一個文件頁面,涵蓋 如何正確使用 Redux 和 Next.js。我們看到很多關於同時使用 Redux、Next 和 App Router 的問題,而這個指南應該有助於提供建議。
(目前,Next.js with-redux
範例仍然顯示過時的模式 - 我們將很快提交一個 PR,以更新它以符合我們的文件指南。)
覆寫相依性
套件需要一段時間才能更新其對等相依性,以允許 Redux 核心 5.0,而在此期間,中間件類型 等變更將導致感知到的不相容性。
大多數程式庫實際上都不會有與 5.0 不相容的做法,但由於對 4.0 的對等相依性,它們最終會引入舊的類型宣告。
這可以透過手動覆寫相依性解析來解決,這受到 npm
和 yarn
的支援。
npm
- overrides
NPM 透過 overrides
欄位在您的 package.json
中支援這項功能。您可以覆寫特定套件的相依性,或確保每個引入 Redux 的套件都收到相同的版本。
{
"overrides": {
"redux-persist": {
"redux": "^5.0.0"
}
}
}
{
"overrides": {
"redux": "^5.0.0"
}
}
yarn
- resolutions
Yarn 透過 resolutions
欄位在 package.json
中支援此功能。就像 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 尚未確定,但使用潛在 API 實作的現有 create.asyncThunk
建立器可能如下所示
const asyncThunkCreator = {
type: ReducerType.asyncThunk,
define(payloadCreator, config) {
return {
type: ReducerType.asyncThunk, // needs to match reducer type, so correct handler can be called
payloadCreator,
...config
}
},
handle(
{
// the key the reducer was defined under
reducerName,
// the autogenerated action type, i.e. `${slice.name}/${reducerName}`
type
},
// the definition from define()
definition,
// methods to modify slice
context
) {
const { payloadCreator, options, pending, fulfilled, rejected, settled } =
definition
const asyncThunk = createAsyncThunk(type, payloadCreator, options)
if (pending) context.addCase(asyncThunk.pending, pending)
if (fulfilled) context.addCase(asyncThunk.fulfilled, fulfilled)
if (rejected) context.addCase(asyncThunk.rejected, rejected)
if (settled) context.addMatcher(asyncThunk.settled, settled)
context.exposeAction(reducerName, asyncThunk)
context.exposeCaseReducer(reducerName, {
pending: pending || noop,
fulfilled: fulfilled || noop,
rejected: rejected || noop,
settled: settled || noop
})
}
}
const createSlice = buildCreateSlice({
creators: {
asyncThunk: asyncThunkCreator
}
})
不過我們不確定有多少人/函式庫會實際使用這個功能,因此歡迎在 Github 議題 提供任何意見回饋!
createSlice.selector
選擇器工廠
內部有一些疑慮,關於 createSlice.selectors
是否充分支援記憶化選擇器。您可以提供一個記憶化選擇器給 createSlice.selectors
設定,但您會受限於那個執行個體。
const todoSlice = createSlice({
name: 'todos',
initialState: {
todos: [] as Todo[]
},
reducers: {},
selectors: {
selectTodosByAuthor = createSelector(
(state: TodoState) => state.todos,
(state: TodoState, author: string) => author,
(todos, author) => todos.filter(todo => todo.author === author)
)
}
})
export const { selectTodosByAuthor } = todoSlice.selectors
由於 createSelector
的預設快取大小為 1,如果在多個元件中使用不同的引數呼叫,可能會導致快取問題。一個典型的解決方案(不使用 createSlice
)是 選擇器工廠
export const makeSelectTodosByAuthor = () =>
createSelector(
(state: RootState) => state.todos.todos,
(state: RootState, author: string) => author,
(todos, author) => todos.filter(todo => todo.author === author)
)
function AuthorTodos({ author }: { author: string }) {
const selectTodosByAuthor = useMemo(makeSelectTodosByAuthor, [])
const todos = useSelector(state => selectTodosByAuthor(state, author))
}
當然,有了 createSlice.selectors
,這不再可能,因為你在建立切片時需要選擇器實例。
在 2.0.0 中,我們沒有解決方案 - 已經提出了幾個 API(PR 1,PR 2),但尚未決定。如果你希望看到這些功能,請考慮在 Github 討論 中提供意見!
3.0 - RTK Query
RTK 2.0 主要專注於核心和工具組的變更。現在 2.0 已經發布,我們希望將重點轉移到 RTK Query,因為仍有一些需要解決的粗糙邊緣 - 其中一些可能需要重大變更,因此需要 3.0 版本。
如果你對此有任何意見,請考慮在 RTK Query API 痛點和粗糙點回饋串 中發表意見!