使用指南
Redux 核心程式庫刻意不帶有意見。它讓您可以決定如何處理所有事情,例如儲存設定、狀態包含的內容,以及如何建立 Reducer。
在某些情況下,這很好,因為它提供了靈活性,但並非總是需要這種靈活性。有時我們只想用最簡單的方式開始,並具有一些開箱即用的良好預設行為。或者,您正在編寫一個較大的應用程式,並發現自己編寫了一些類似的程式碼,您希望減少手動編寫這些程式碼的數量。
如 快速入門 頁面所述,Redux Toolkit 的目標是簡化常見的 Redux 使用案例。它並非旨在成為您可能想用 Redux 執行的所有事情的完整解決方案,但它應該讓您需要編寫的大量 Redux 相關程式碼變得更簡單(或在某些情況下,完全消除一些手寫程式碼)。
Redux Toolkit 會匯出數個可於應用程式中使用的個別函式,並加入對 Redux 中常見其他套件的依賴項(例如 Reselect 和 Redux-Thunk)。這讓你可以在自己的應用程式中決定如何使用這些套件,無論是全新專案或更新大型現有應用程式。
讓我們來看看 Redux Toolkit 可以如何協助改善你的 Redux 相關程式碼。
儲存設定
每個 Redux 應用程式都需要設定並建立 Redux 儲存。這通常涉及下列步驟
- 匯入或建立根部 reducer 函式
- 設定中間件,可能至少包含一個中間件來處理非同步邏輯
- 設定 Redux DevTools 擴充功能
- 可能根據應用程式是為開發或製作而建構,來變更部分邏輯
手動儲存設定
Redux 文件中的 設定你的儲存 頁面中的以下範例顯示典型的儲存設定流程
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
此範例可讀,但流程並不總是那麼簡單
- 基本的 Redux
createStore
函式會採用位置引數:(rootReducer, preloadedState, enhancer)
。有時很容易忘記哪個參數是什麼。 - 設定中間件和增強器的流程可能會令人困惑,特別是如果你嘗試加入多個設定項目時。
- Redux DevTools 擴充功能文件最初建議使用 一些手寫程式碼,檢查全域命名空間以查看擴充功能是否可用。許多使用者會複製並貼上這些程式碼片段,這會讓設定程式碼更難以閱讀。
使用 configureStore
簡化儲存設定
configureStore
透過下列方式協助解決這些問題
- 擁有具有「命名」參數的選項物件,這會更容易閱讀
- 讓您提供您想新增至儲存的 middleware 和 enhancer 陣列,並自動為您呼叫
applyMiddleware
和compose
- 自動啟用 Redux DevTools 擴充功能
此外,configureStore
預設會新增一些 middleware,每個都有特定的目標
redux-thunk
是最常使用的 middleware,用於處理元件外部的同步和非同步邏輯- 在開發中,middleware 會檢查常見錯誤,例如變異狀態或使用不可序列化值。
這表示儲存設定碼本身會短一些且更容易閱讀,而且您還能獲得良好的預設行為。
使用它的最簡單方法是僅傳遞根 reducer 函式作為名為 reducer
的參數
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer,
})
export default store
您也可以傳遞一個包含 「切片 reducer」 的完整物件,而 configureStore
會為您呼叫 combineReducers
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
})
export default store
請注意,這僅適用於一層 reducer。如果您想巢狀 reducer,您需要自己呼叫 combineReducers
來處理巢狀。
如果您需要自訂儲存設定,您可以傳遞額外的選項。以下是使用 Redux Toolkit 的熱重載範例
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
preloadedState,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(monitorReducersEnhancer),
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
如果您提供 middleware
參數,configureStore
將只使用您列出的任何 middleware。如果您想要一些自訂 middleware 和 所有預設值,您可以使用 callback 符號,呼叫 getDefaultMiddleware
,並將結果包含在您回傳的 middleware
陣列中。
撰寫 Reducer
Reducer 是最重要的 Redux 概念。典型的 reducer 函式需要
- 查看動作物件的
type
欄位,以了解它應該如何回應 - 不可變地更新其狀態,透過複製需要變更的狀態部分,並僅修改這些副本
雖然您可以在 reducer 中 使用任何您想要的條件式邏輯,但最常見的方法是 switch
陳述式,因為這是一種處理單一欄位多個可能值的直接方法。然而,許多人不喜歡 switch 陳述式。Redux 文件顯示 撰寫一個根據動作類型作為查詢表的函式 的範例,但讓使用者自行自訂該函式。
撰寫 reducer 的其他常見痛點與不可變地更新狀態有關。JavaScript 是一種可變語言,手動更新巢狀不可變資料很困難,而且很容易出錯。
使用 createReducer
簡化 Reducer
由於「查詢表」方法很受歡迎,Redux Toolkit 包含一個類似 Redux 文件中所示的 createReducer
函式。但是,我們的 createReducer
工具程式有一些特殊的「魔法」,讓它變得更好。它在內部使用 Immer 函式庫,讓你可以撰寫「變異」某些資料的程式碼,但實際上是以不可變的方式套用更新。這讓意外變異 Reducer 中的狀態變得幾乎不可能。
一般來說,任何使用 switch
陳述式的 Redux Reducer 都可以轉換為直接使用 createReducer
。switch 中的每個 case
都會變成傳遞給 createReducer
的物件中的金鑰。不可變的更新邏輯(例如散布物件或複製陣列)可能可以轉換為直接「變異」。保留不可變的更新並傳回更新的副本也是可以的。
以下是一些你可以使用 createReducer
的範例。我們將從一個典型的「待辦事項清單」Reducer 開始,它使用 switch 陳述式和不可變更新
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return state.concat(action.payload)
}
case 'TOGGLE_TODO': {
const { index } = action.payload
return state.map((todo, i) => {
if (i !== index) return todo
return {
...todo,
completed: !todo.completed,
}
})
}
case 'REMOVE_TODO': {
return state.filter((todo, i) => i !== action.payload.index)
}
default:
return state
}
}
請注意,我們特別呼叫 state.concat()
以傳回一個包含新待辦事項條目的複製陣列,state.map()
以傳回一個用於切換案例的複製陣列,並使用物件散布運算子來複製需要更新的待辦事項。
使用 createReducer
,我們可以大幅縮短範例
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// "mutate" the object by overwriting a field
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter((todo, i) => i !== action.payload.index)
})
})
當嘗試更新深度巢狀狀態時,「變異」狀態的能力特別有幫助。這個複雜且令人痛苦的程式碼
case "UPDATE_VALUE":
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
可以簡化為
updateValue(state, action) {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
}
好多了!
使用 createReducer
的注意事項
雖然 Redux Toolkit 的 createReducer
函式真的很有幫助,但請記住
- 「變異」程式碼只在我們的
createReducer
函式內正確運作 - Immer 不同時允許你「變異」草稿狀態和傳回新的狀態值
請參閱 createReducer
API 參考 以取得更多詳細資料。
撰寫 Action 建構函式
Redux 鼓勵你 撰寫「action 建構函式」函式,以封裝建立 action 物件的程序。雖然這不是絕對必要的,但它是 Redux 使用的標準部分。
大多數 action 建構函式都很簡單。它們會接收一些參數,並傳回一個具有特定 type
欄位和 action 內部參數的 action 物件。這些參數通常會放入稱為 payload
的欄位中,這是 Flux 標準 Action 慣例的一部分,用於整理 action 物件的內容。典型的 action 建構函式可能如下所示
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}
使用 createAction
定義動作建立器
手動撰寫動作建立器會很繁瑣。Redux Toolkit 提供一個名為 createAction
的函數,它只會產生一個使用指定動作類型的動作建立器,並將其參數轉換為 payload
欄位
const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
createAction
也接受「準備回呼」參數,它允許您自訂產生的 payload
欄位,並選擇性地加入 meta
欄位。請參閱 createAction
API 參考,了解使用準備回呼定義動作建立器的詳細資訊。
將動作建立器用作動作類型
Redux 減速器需要尋找特定的動作類型,以確定它們應如何更新其狀態。通常,這是透過分別定義動作類型字串和動作建立器函數來完成的。Redux Toolkit createAction
函數透過在動作建立器上定義動作類型為 type
欄位,讓這件事變得更容易。
const actionCreator = createAction('SOME_ACTION_TYPE')
console.log(actionCreator.type)
// "SOME_ACTION_TYPE"
const reducer = createReducer({}, (builder) => {
// if you use TypeScript, the action type will be correctly inferred
builder.addCase(actionCreator, (state, action) => {})
// Or, you can reference the .type field:
// if using TypeScript, the action type cannot be inferred that way
builder.addCase(actionCreator.type, (state, action) => {})
})
這表示您不必撰寫或使用單獨的動作類型變數,或重複動作類型的名稱和值,例如 const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"
。
如果您想在 switch 語句中使用其中一個動作建立器,您需要自行參照 actionCreator.type
const actionCreator = createAction('SOME_ACTION_TYPE')
const reducer = (state = {}, action) => {
switch (action.type) {
// ERROR: this won't work correctly!
case actionCreator: {
break
}
// CORRECT: this will work as expected
case actionCreator.type: {
break
}
}
}
建立狀態區塊
Redux 狀態通常整理成「區塊」,由傳遞給 combineReducers
的減速器定義
import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
})
在此範例中,users
和 posts
都會被視為「區塊」。兩個減速器
- 「擁有」狀態的一部份,包括初始值是什麼
- 定義該狀態如何更新
- 定義哪些特定動作會導致狀態更新
常見的做法是在自己的檔案中定義區塊的減速器函數,並在第二個檔案中定義動作建立器。由於兩個函數都需要參照相同的動作類型,因此這些動作類型通常定義在第三個檔案中,並在兩個地方匯入
// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// omit implementation
}
default:
return state
}
}
這裡唯一真正必要的部份是減速器本身。考慮其他部份
- 我們可以在兩個地方將動作類型寫成內嵌字串
- 動作建立器很好,但使用 Redux 並非必要 - 元件可以略過提供
mapDispatch
參數給connect
,並自行呼叫this.props.dispatch({type : "CREATE_POST", payload : {id : 123, title : "Hello World"}})
- 我們甚至撰寫多個檔案的唯一原因,是因為根據功能區分程式碼很常見
「ducks 檔案結構」建議將特定區塊的所有 Redux 相關邏輯放入單一檔案中,如下所示
// postsDuck.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// Omit actual code
break
}
default:
return state
}
}
這簡化了事情,因為我們不需要有多個檔案,而且可以移除動作類型常數的重複匯入。但是,我們仍然必須手動撰寫動作類型和動作建立函式。
在物件中定義函式
在現代 JavaScript 中,有幾種合法的方式可以在物件中定義鍵和函式(這不特定於 Redux),而且你可以混合和搭配不同的鍵定義和函式定義。例如,以下都是物件中定義函式的合法方式
const keyName = "ADD_TODO4";
const reducerObject = {
// Explicit quotes for the key name, arrow function for the reducer
"ADD_TODO1" : (state, action) => { }
// Bare key with no quotes, function keyword
ADD_TODO2 : function(state, action){ }
// Object literal function shorthand
ADD_TODO3(state, action) { }
// Computed property
[keyName] : (state, action) => { }
}
使用「物件文字函式簡寫」可能是最簡短的程式碼,但請隨時使用你想要的任何方法。
使用 createSlice
簡化區塊
為了簡化此程序,Redux Toolkit 包含一個 createSlice
函式,它會根據你提供的簡化器函式名稱自動產生動作類型和動作建立函式。
以下是使用 createSlice
的貼文範例
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
console.log(postsSlice)
/*
{
name: 'posts',
actions : {
createPost,
updatePost,
deletePost,
},
reducer
}
*/
const { createPost } = postsSlice.actions
console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
createSlice
查看在 reducers
欄位中定義的所有函式,並針對提供的每個「案例簡化器」函式,產生一個動作建立函式,該函式使用簡化器的名稱作為動作類型本身。因此,createPost
簡化器變成了 "posts/createPost"
動作類型,而 createPost()
動作建立函式會傳回具有該類型的動作。
匯出和使用區塊
大部分時間,你會想要定義一個區塊,並匯出其動作建立函式和簡化器。建議使用 ES6 解構和匯出語法來執行此操作
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
// Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer
如果你喜歡,也可以直接匯出區塊物件本身。
以這種方式定義的區塊在概念上與「Redux Ducks」模式非常相似,用於定義和匯出動作建立函式和簡化器。但是,在匯入和匯出區塊時,有一些潛在的缺點需要注意。
首先,Redux 動作類型不應專屬於單一區塊。在概念上,每個區塊 reducer「擁有」Redux 狀態中的區塊,但它應該能夠偵聽任何動作類型並適當地更新其狀態。例如,許多不同的區塊可能希望透過清除資料或重設回初始狀態值來回應「使用者已登出」動作。在設計狀態形狀和建立區塊時請記住這一點。
其次,如果兩個模組嘗試互相匯入,JS 模組可能會出現「循環參考」問題。這可能會導致匯入未定義,這可能會中斷需要該匯入的程式碼。特別是在「ducks」或區塊的情況下,如果在兩個不同檔案中定義的區塊都想要回應另一個檔案中定義的動作,就會發生這種情況。
此 CodeSandbox 範例示範了這個問題
如果您遇到這種情況,您可能需要以避免循環參考的方式重新調整程式碼結構。這通常需要將共用程式碼萃取到兩個模組都可以匯入和使用的獨立共用檔案中。在此情況下,您可以在獨立檔案中使用 createAction
定義一些共用動作類型,將這些動作建立函式匯入每個區塊檔案,並使用 extraReducers
參數處理它們。
文章 如何在 JS 中一勞永逸地修復循環依賴問題 提供了其他資訊和範例,可以協助解決此問題。
非同步邏輯和資料擷取
使用中間件來啟用非同步邏輯
Redux 儲存本身對非同步邏輯一無所知。它只知道如何同步發送動作、透過呼叫根 reducer 函式來更新狀態,並通知 UI 有所變更。任何非同步性都必須在儲存外部發生。
但是,如果您希望非同步邏輯透過發送或檢查目前的儲存狀態與儲存互動,這時 Redux 中間件 就派上用場了。它們擴充了儲存,並允許您
- 在發送任何動作時執行額外邏輯(例如記錄動作和狀態)
- 暫停、修改、延遲、替換或終止已派送的動作
- 撰寫可存取
dispatch
和getState
的額外程式碼 - 透過攔截
dispatch
並改派送實際動作物件,教導dispatch
如何接受一般動作物件以外的其他值,例如函式和承諾
使用中間件最常見的原因是允許不同類型的非同步邏輯與儲存體互動。這讓你可以撰寫可派送動作並檢查儲存體狀態的程式碼,同時將該邏輯與使用者介面分開。
Redux 有許多種非同步中間件,每種都讓你使用不同的語法撰寫邏輯。最常見的非同步中間件是
redux-thunk
,它讓你撰寫可能直接包含非同步邏輯的純函式redux-saga
,它使用回傳行為描述的產生器函式,以便中間件執行這些函式redux-observable
,它使用 RxJS 可觀察函式庫建立處理動作的函式鏈
Redux Toolkit 的 RTK Query 資料擷取 API 是為 Redux 應用程式打造的資料擷取和快取解決方案,且可以消除撰寫任何 thunk 或 reducer 來管理資料擷取的需求。我們鼓勵你試用看看,了解它是否可以簡化你自己的應用程式中的資料擷取程式碼!
如果你確實需要自己撰寫資料擷取邏輯,我們建議 使用 Redux Thunk 中間件作為標準方法,因為它足以應付大多數典型使用案例(例如基本的 AJAX 資料擷取)。此外,在 thunk 中使用 async/await
語法讓它們更容易閱讀。
Redux Toolkit configureStore
函式 預設自動設定 thunk 中間件,因此你可以立即開始撰寫 thunk 作為應用程式程式碼的一部分。
在區段中定義非同步邏輯
Redux Toolkit 目前未提供任何用於撰寫 thunk 函式的特殊 API 或語法。特別是,它們無法定義為 createSlice()
呼叫的一部分。你必須將它們與 reducer 邏輯分開撰寫,這與純 Redux 程式碼完全相同。
Thunk 通常會派送純粹的動作,例如 dispatch(dataLoaded(response.data))
。
許多 Redux 應用程式使用「按類型分類資料夾」的方法來建構其程式碼。在這種結構中,thunk 動作建立器通常會在「動作」檔案中定義,與純粹的動作建立器並列。
由於我們沒有單獨的「動作」檔案,因此將這些 thunk 直接寫在我們的「區塊」檔案中是有意義的。這樣,它們可以存取區塊中的純粹動作建立器,而且很容易找到 thunk 函數所在的位置。
包含 thunk 的典型區塊檔案看起來像這樣
// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: [],
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
},
},
})
// Destructure and export the plain action creators
export const { usersLoading, usersReceived } = usersSlice.actions
// Define a thunk that dispatches those action creators
const fetchUsers = () => async (dispatch) => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}
Redux 資料擷取模式
Redux 的資料擷取邏輯通常遵循可預測的模式
- 在請求之前會派送「開始」動作,以表示請求正在進行中。這可以用於追蹤載入狀態,允許略過重複請求,或在使用者介面中顯示載入指標。
- 進行非同步請求
- 根據請求結果,非同步邏輯會派送包含結果資料的「成功」動作,或包含錯誤詳細資料的「失敗」動作。縮減器邏輯在這兩種情況下都會清除載入狀態,並處理成功案例的結果資料,或儲存錯誤值以供潛在顯示。
這些步驟不是必需的,但 在 Redux 教學課程中建議作為建議模式。
典型的實作可能看起來像
const getRepoDetailsStarted = () => ({
type: 'repoDetails/fetchStarted',
})
const getRepoDetailsSuccess = (repoDetails) => ({
type: 'repoDetails/fetchSucceeded',
payload: repoDetails,
})
const getRepoDetailsFailed = (error) => ({
type: 'repoDetails/fetchFailed',
error,
})
const fetchIssuesCount = (org, repo) => async (dispatch) => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}
但是,使用這種方法撰寫程式碼很繁瑣。每種類型的請求都需要重複類似的實作
- 需要針對三種不同的情況定義唯一的動作類型
- 每種類型的動作通常都有對應的動作建立器函數
- 必須撰寫一個 thunk,以正確的順序派送正確的動作
createAsyncThunk
透過產生動作類型和動作建立器,以及產生派送這些動作的 thunk,來抽象化此模式。
使用 createAsyncThunk
的非同步請求
作為開發人員,您可能最關心進行 API 請求所需的實際邏輯、Redux 動作歷程記錄中顯示哪些動作類型名稱,以及縮減器應如何處理擷取的資料。定義多個動作類型和按正確順序派送動作的重複性細節並非重點。
createAsyncThunk
簡化了這個流程 - 您只需要提供一個字串作為動作類型前綴,以及一個執行實際非同步邏輯並傳回承諾結果的酬載建立器回呼。作為回報,createAsyncThunk
會提供一個 thunk,它會根據您傳回的承諾處理派送正確動作,以及您可以在縮減器中處理的動作類型
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
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) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
thunk 動作建立器接受一個單一引數,它將作為第一個引數傳遞給您的酬載建立器回呼。
有效負載建立器也會收到一個 thunkAPI
物件,其中包含通常傳遞給標準 Redux thunk 函式的參數,以及一個自動產生的唯一隨機要求 ID 字串和一個 AbortController.signal
物件
interface ThunkAPI {
dispatch: Function
getState: Function
extra?: any
requestId: string
signal: AbortSignal
}
您可以在有效負載回呼中根據需要使用這些參數來確定最終結果應為何。
管理正規化資料
大多數應用程式通常會處理深度巢狀或關聯的資料。正規化資料的目標是有效率地整理狀態中的資料。這通常是透過將集合儲存在物件中,其金鑰為 id
,同時儲存這些 id
的排序陣列來完成。如需更深入的說明和更多範例,Redux 文件頁面上的「正規化狀態形狀」 有很好的參考。
手動正規化
正規化資料不需要任何特殊函式庫。以下是一個基本範例,說明您如何使用手寫邏輯正規化從 fetchAll
API 要求傳回的資料,其形狀為 { users: [{id: 1, first_name: 'normalized', last_name: 'person'}] }
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
return response.data
})
export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
// reduce the collection by the id property into a shape of { 1: { ...user }}
const byId = action.payload.users.reduce((byId, user) => {
byId[user.id] = user
return byId
}, {})
state.entities = byId
state.ids = Object.keys(byId)
})
},
})
儘管我們有能力撰寫此程式碼,但它確實會重複,特別是如果您處理多種類型的資料時。此外,此範例僅處理將條目載入狀態,而不更新它們。
使用 normalizr
正規化
normalizr
是正規化資料的熱門現有函式庫。您可以在沒有 Redux 的情況下單獨使用它,但它通常與 Redux 搭配使用。典型的用法是從 API 回應中格式化集合,然後在您的簡化器中處理它們。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { normalize, schema } from 'normalizr'
import userAPI from './userAPI'
const userEntity = new schema.Entity('users')
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// Normalize the data before passing it to our reducer
const normalized = normalize(response.data, [userEntity])
return normalized.entities
})
export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.entities = action.payload.users
state.ids = Object.keys(action.payload.users)
})
},
})
與手寫版本一樣,這不處理將其他條目新增到狀態中,或稍後更新它們 - 它只是載入收到的一切。
使用 createEntityAdapter
正規化
Redux Toolkit 的 createEntityAdapter
API 提供一種標準化方式,透過取得集合並將其放入 { ids: [], entities: {} }
形狀中,將您的資料儲存在區段中。除了這個預先定義的狀態形狀外,它還會產生一組簡化器函式和選取器,知道如何處理資料。
import {
createSlice,
createAsyncThunk,
createEntityAdapter,
} from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data
})
export const updateUser = createAsyncThunk('users/updateOne', async (arg) => {
const response = await userAPI.updateUser(arg)
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data
})
export const usersAdapter = createEntityAdapter()
// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
// If you want to track 'loading' or other keys, you would initialize them here:
// `getInitialState({ loading: false, activeRequestId: null })`
const initialState = usersAdapter.getInitialState()
export const slice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload
usersAdapter.updateOne(state, { id, changes })
})
},
})
const reducer = slice.reducer
export default reducer
export const { removeUser } = slice.actions
您可以在 CodeSandbox 上查看此範例用法的完整程式碼
將 createEntityAdapter
與正規化函式庫搭配使用
如果您已經使用 normalizr
或其他正規化函式庫,可以考慮與 createEntityAdapter
一起使用。以上面的範例為基礎,以下是我們如何使用 normalizr
格式化有效負載,然後利用 createEntityAdapter
提供的公用程式。
預設情況下,setAll
、addMany
和 upsertMany
CRUD 方法會預期陣列中的實體。不過,它們也允許您傳入形狀為 { 1: { id: 1, ... }}
的物件作為替代方案,這使得插入預先正規化的資料變得更容易。
// features/articles/articlesSlice.js
import {
createSlice,
createEntityAdapter,
createAsyncThunk,
createSelector,
} from '@reduxjs/toolkit'
import fakeAPI from '../../services/fakeAPI'
import { normalize, schema } from 'normalizr'
// Define normalizr entity schemas
export const userEntity = new schema.Entity('users')
export const commentEntity = new schema.Entity('comments', {
commenter: userEntity,
})
export const articleEntity = new schema.Entity('articles', {
author: userEntity,
comments: [commentEntity],
})
const articlesAdapter = createEntityAdapter()
export const fetchArticle = createAsyncThunk(
'articles/fetchArticle',
async (id) => {
const data = await fakeAPI.articles.show(id)
// Normalize the data so reducers can load a predictable payload, like:
// `action.payload = { users: {}, articles: {}, comments: {} }`
const normalized = normalize(data, articleEntity)
return normalized.entities
}
)
export const slice = createSlice({
name: 'articles',
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Handle the fetch result by inserting the articles here
articlesAdapter.upsertMany(state, action.payload.articles)
})
},
})
const reducer = slice.reducer
export default reducer
// features/users/usersSlice.js
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'
const usersAdapter = createEntityAdapter()
export const slice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// And handle the same fetch result by inserting the users here
usersAdapter.upsertMany(state, action.payload.users)
})
},
})
const reducer = slice.reducer
export default reducer
// features/comments/commentsSlice.js
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'
const commentsAdapter = createEntityAdapter()
export const slice = createSlice({
name: 'comments',
initialState: commentsAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Same for the comments
commentsAdapter.upsertMany(state, action.payload.comments)
})
},
})
const reducer = slice.reducer
export default reducer
您可以在 CodeSandbox 上查看此範例 normalizr
使用的完整程式碼
與 createEntityAdapter
一起使用選取器
實體適配器提供選取器工廠,可為您產生最常見的選取器。根據上述範例,我們可以像這樣將選取器新增到我們的 usersSlice
// Rename the exports for readability in component usage
export const {
selectById: selectUserById,
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectTotalUsers,
} = usersAdapter.getSelectors((state) => state.users)
然後,您可以在元件中使用這些選取器,如下所示
import React from 'react'
import { useSelector } from 'react-redux'
import { selectTotalUsers, selectAllUsers } from './usersSlice'
import styles from './UsersList.module.css'
export function UsersList() {
const count = useSelector(selectTotalUsers)
const users = useSelector(selectAllUsers)
return (
<div>
<div className={styles.row}>
There are <span className={styles.value}>{count}</span> users.{' '}
{count === 0 && `Why don't you fetch some more?`}
</div>
{users.map((user) => (
<div key={user.id}>
<div>{`${user.first_name} ${user.last_name}`}</div>
</div>
))}
</div>
)
}
指定替代 ID 欄位
預設情況下,createEntityAdapter
假設您的資料在 entity.id
欄位中具有唯一的 ID。如果您的資料集將其 ID 儲存在不同的欄位中,您可以傳入 selectId
參數,以傳回適當的欄位。
// In this instance, our user data always has a primary key of `idx`
const userData = {
users: [
{ idx: 1, first_name: 'Test' },
{ idx: 2, first_name: 'Two' },
],
}
// Since our primary key is `idx` and not `id`,
// pass in an ID selector to return that field instead
export const usersAdapter = createEntityAdapter({
selectId: (user) => user.idx,
})
排序實體
createEntityAdapter
提供 sortComparer
參數,您可以利用它來對狀態中的 ids
集合進行排序。當您想要保證排序順序,而資料沒有預先排序時,這會非常有用。
// In this instance, our user data always has a primary key of `id`, so we do not need to provide `selectId`.
const userData = {
users: [
{ id: 1, first_name: 'Test' },
{ id: 2, first_name: 'Banana' },
],
}
// Sort by `first_name`. `state.ids` would be ordered as
// `ids: [ 2, 1 ]`, since 'B' comes before 'T'.
// When using the provided `selectAll` selector, the result would be sorted:
// [{ id: 2, first_name: 'Banana' }, { id: 1, first_name: 'Test' }]
export const usersAdapter = createEntityAdapter({
sortComparer: (a, b) => a.first_name.localeCompare(b.first_name),
})
使用不可序列化資料
Redux 的核心使用原則之一是 您不應在狀態或動作中放置不可序列化的值。
不過,就像大多數規則一樣,也有例外。有時您可能必須處理需要接受不可序列化資料的動作。這應該非常少見,並且只有在必要時才執行,而且這些不可序列化的有效負載不應透過簡化器進入您的應用程式狀態。
serializability dev check 中間件 會在偵測到動作或狀態中存在無法序列化的值時自動發出警告。我們建議保持此中間件處於啟用狀態,以避免意外發生錯誤。不過,如果你確實需要關閉這些警告,你可以透過設定中間件來忽略特定動作類型或動作和狀態中的欄位,進而自訂中間件
configureStore({
//...
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['your/action/type'],
// Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}),
})
與 Redux-Persist 搭配使用
如果使用 Redux-Persist,你應該特別忽略它所發出的所有動作類型
import { configureStore } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
import App from './App'
import rootReducer from './reducers'
const persistConfig = {
key: 'root',
version: 1,
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
let persistor = persistStore(store)
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root'),
)
此外,你可以透過在呼叫 persistor.purge() 時,將額外 reducer 加入你想要清除的特定區塊,來清除任何已持續化的狀態。當你希望在發出登出動作時清除已持續化的狀態時,這項功能特別有用。
import { PURGE } from "redux-persist";
...
extraReducers: (builder) => {
builder.addCase(PURGE, (state) => {
customEntityAdapter.removeAll(state);
});
}
強烈建議將你已使用 RTK Query 設定的任何 API 加入黑名單。如果 API 區塊 reducer 未加入黑名單,API 快取將會自動持續化並還原,這可能會導致你收到來自不再存在的元件的虛擬訂閱。設定方式應如下所示
const persistConfig = {
key: 'root',
version: 1,
storage,
blacklist: [pokemonApi.reducerPath],
}
請參閱 Redux Toolkit #121:如何與 Redux-Persist 搭配使用? 和 Redux-Persist #988:無法序列化的值錯誤 以取得進一步討論。
與 React-Redux-Firebase 搭配使用
從 3.x 開始,RRF 會在大部分動作和狀態中包含時間戳記值,但有一些 PR 可能會改善 4.x 的行為。
可以搭配此行為使用的設定如下所示
import { configureStore } from '@reduxjs/toolkit'
import {
getFirebase,
actionTypes as rrfActionTypes,
} from 'react-redux-firebase'
import { constants as rfConstants } from 'redux-firestore'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [
// just ignore every redux-firebase and react-redux-firebase action type
...Object.keys(rfConstants.actionTypes).map(
(type) => `${rfConstants.actionsPrefix}/${type}`,
),
...Object.keys(rrfActionTypes).map(
(type) => `@@reactReduxFirebase/${type}`,
),
],
ignoredPaths: ['firebase', 'firestore'],
},
thunk: {
extraArgument: {
getFirebase,
},
},
}),
})
export default store