跳至主要内容

extraReducer: reducer 的進化版

extraReducer 行為

extraReducer 會偵測 全局 的 action,再對自己身處的 slice 做 state 更新
extraReducer 也可以處理 非同步 操作,需要搭配 createAsyncThunk
extraReducer 有使用 Immer 協助,可以直接做類似 mutating 的操作
extraReducer 可以定義預設行為

That means many different slice reducers can all respond to the same dispatched action, and each slice can update its own state if needed!

與 reducer 不同

extraReducer 不會產生新的 action type
如果 reducer、extraReducer 對同一個 action type 定義,會以 reducer 執行

extraReducer 使用

本篇文主要介紹兩個 extraReducer 的功用 處理非同步邏輯監聽全域的 Action
實際上還有其他應用,可以直接看官網

extraReducer 處理非同步

以下例子為,機器狀態必須透過 API (非同步)的回應來取得
fetchMachineData 必須處理非同步的事件,因此要用 createAsyncThunk 創建。當請求成功時,會進入 fulfilled 的邏輯,反之,失敗會進入 rejectedcreateAsyncThunk 的介紹可以參考這篇

machineSlice.ts
export const fetchMachineData = createAsyncThunk<MachineState, string>('../fetchData', async (stateId: string) => {
if (MACHINE_STATE.includes(stateId)) {
const resp = await ...;
return parseData(resp.payload) as MachineState;
}

return {
...
} as MachineState;
});


const machineSlice = createSlice({
name: 'machine',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchMachineData.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchMachineData.fulfilled, (state, action) => {
const { data } = action.payload;
state.data = data;

})
.addCase(fetchMachineData.rejected, (state) => {
state.data = null;
});
},
});

我們在 machineSlice 定義好從後端獲取資料的成功、失敗後的處理,就可以用 dispatch(fetchMachineData(machineState)) 觸發 extraReducer。當 HTTP 請求發出後,Redux Toolkit 會派發 fetchMachineData/pending 給 extraReducer,通知目前正在加載,extraReducer 就會進入 addCase(fetchMachineData.pending) 這個邏輯,如果不需要特別處理 Loading,那這個 case 不給也可以。

請求完成後,會根據結果成功與否走 fulfilled or rejected

addCase 的寫法是以鏈(chain)的形式接續。

extraReducer 處理全域、跨檔案監聽 action

要做到這個行為,可以使用 addCase 或者 addMatcher,但這邊先只說明 addCase,因為 addMatcher 的判斷比較多樣,可以監聽多個 action。

addCase

以下例子為 Redux 官方示範,當 User 登出後,要把資料清空。我們將登出的邏輯 userLoggedOut reducer 定義在 authSlice.ts,但資料 state 定義在 postSlice.ts,我們可以利用 extraReducer 在 postSlice.ts 監聽 userLoggedOut

authSlice.ts
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
userLoggedIn(state, action: PayloadAction<string>) {
state.username = action.payload;
},
userLoggedOut(state) {
state.username = null;
},
},
});
postSlice.ts
import { userLoggedOut } from '@/features/auth/authSlice';

const postSlice = createSlice({
name: 'posts',
initialState,
reducers: {
...
},
extraReducers: (build) => {
build.addCase(userLoggedOut, () => {
return [];
});
}
})

不管 userLoggedOut 在哪邊被 dispatch,都會通知 postSlice.ts 的 extraReducer,經由 return [] 將 post 的 state 更新為空陣列。

extraReducer 預設行為 - defaultCase

我覺得這個東西很危險!
addDefaultCase 通常擺在 extraReducer 最下方,用來處理預設的行為,可以視為類似 JS Switch 語法的 default。
危險的點在於,只要沒被 addCaseaddMatcher 處理的 action type,都會走向 addDefaultCase,但 extraReducer 是監聽整個 global 的 action,我自己是覺得是這個判定導致 addDefaultCase 不太好用,很容易出錯。

注意

所有 action 都會被 Redux 廣播到每個 slice 的 reducer。

在讀 reducer 的時候,我以為 action 只會發送的自己 slice 的 reducer,但為了達成可以偵測其他 action 的操作,extraReducer 使得 Redux 必須把所有的 action 都發送到全局裡每個 reducer,而這個想法仍有誤。

從 extraReducer 看回 Redux 設計

其實即使沒有 extraReducer,Redux 本來就會把所有 action 廣播給每個 reducer,這是為了達到靈活跟彈性,以及 Redux State 是全局可讀取並經由特定方式更新的原則。