在學習 Redux 和 Redux Toolkit 時,最頭痛的莫過於處理非同步操作,也因此有了這一篇文章。在 Redux Toolkit 中的 createAsyncThunk
函數中,有個關鍵字 Thunk,其實就是來自於內建的工具 Redux-Thunk。
在軟體領域中,"Thunk" 是一個常用的術語,它指的是一種用於延遲計算,或將運算延後執行的程式碼片段。它通常用於函數式編程,或編譯器的設計中。
這裡就要稍微提到中介軟體(Middleware)的概念。Middleware 在 Redux 中,指的是在 dispatch 派發任務後、在 action 實際執行前,中間這段可以額外處理的過程。整個概念,其實就和 Thunk 本身的含意相似。
在 Redux Toolkit 裡,處理異步的資料處理,我們可以使用 createAsyncThunk
這個 API。createAsyncThunk
其實本質是一個 Action creator,但透過 dispatch 前處理異步的行動,再將資料提供給 dispatch 來執行派發,以達到 Middleware 處理資料的目的。
要實現 Thunk 總共有三個步驟,分別是:
createAsyncThunk
建立 action 與異步資料處理extraReducer
處理 action 派發後續使用的 reducerdispatch
實際派發,執行 reducer在瞭解具體的案例前,先讓我們瞭解 createAsyncThunk
如何使用吧!
在這個區段,為了讓讀者瞭解完整 Thunk 運作的流程,總共會說明三個部分。分別是:
A: createAsyncThunk 參數輸入
B: 如何處理 Thunk 返回值
C: 完整的 Thunk 派發過程
createAsyncThunk
總共有三個參數,分別是:
Type
PayloaderCreator(arg, ThunkAPI)
options
下面會逐步說明每一個參數的用途,並說明可以如何設定每個參數。
用於設定額外的 Action Type,讓 Redux 可以辨識你使用的 Action 是什麼。通常都會以 “slice/action”
的寫法。且使用 Thunk 時,Redux 會自動生成額外的 pending/fulfilled/rejected
三個 Action,會於後續詳細說明。
需要傳入一個 Promise,該函式用於放置我們的異步操作,通常會使用 Async 函式來處理。payloadCreator
會傳入兩個參數,分別是 arg
和 ThunkAPI
。
arg
:便是我們在使用 Action 時,接收傳入資料作為參數的地方ThunkAPI
:則是 Redux 提供一整組的 API,例如獲取 getState、dispatch、extra 傳入其他參數等 API。Options 會傳入一個物件,常見的有下列一些屬性,其他可以參考官方文件:
condition
:用於中斷建立 payload creator 的 condition 屬性。dispatchConditionRejection
: true,用於強制返回一個 rejected 的 Action 並派發。在處理完資料後,Thunk 會返回一個標準的 thunk Action creator。總共會返回四個 Action creator:
“slice/action”
:會創建一個 Thunk 本身的 action creator,用於派發 Thunk 的 Action。“slice/action/pending”
:會創建一個用於派發 pending 狀態的 Action creator。“slice/action/fulfilled”
:會創建一個用於派發 fulfilled 狀態的 Action creator。“slice/action/rejected”
:會創建一個用於派發 rejected 狀態的 Action creator。總共會有四個步驟,分別是:
payloadCreator
並等待 Promise resolvefulfilled
並傳遞值給 action.payload
ThunkAPI.rejectWithValue(value)
的錯誤值,並傳遞至 action.payload
。可在 action.error.message
獲得 'Rejected' 字串;若未處理,則派發一個衍生錯誤至 action.error
。在說明完 createAsyncThunk
的函式後,我們就以一個實際案例來說明。到底如何發揮 createAsyncThunk
的威力。
整個案例可以分成三個部分來理解,分別是:
cartSlice
處理 reducer 派發 Action基本上所有的 Thunk 派發邏輯,都遵守上面的這個順序。讀者可以直接參考上方內容,並調整成自己希望使用的模式。
通常我們在撰寫 AsyncThunk 函式時,會將該函式放在 xxxActions.js
的檔案裡,用來特別區分 Action 相關的功能。此處,我們將 Thunk 提供的 type
、payloadCreator
、options
三個參數分開來看。
第一部份 type
:我們設定在 cartSlice
中的 fetchCartData
這個 Action,故使用 "cart/fetchCartData"
來撰寫,這也符合 Redux 設定的規範。
第二部分 payloadCreator
:此處傳入一個異步函式。因為這個 Thunk 是用來獲取資料,因此不需要傳入 Data,故用 _ 來忽略變數。之中對資料進行 json 解析,並處理部分狀態為 fulfilled 但 ok 為 false 的錯誤。
第三部分 options
:可選參數,因為目前並沒有需要終止 Action 或其他需求,因此沒有設定第三個物件參數。
在這個階段,因為 reducer 並非直接設定在 slice 當中,因此 Redux 無法自動從 reducer 中生成 Action Creator,故需要使用 extraReducer 來處理 Thunk 派生的 Action Creator。
目前 Redux Toolkit 全面改用 builder
當作參數傳入,並使用 addCase
來接下 Action。
addCase 本身會有兩個參數:
fetchCartDataThunk.fulfilled
寫法生成 Type。reducer
的地方。此處和內建的 reducers 享有 immer.js 帶來的便利,因此可以直接修改 state
來更新數據。附註:放入 dispatch
是為了避免 Prettier 跳出警示,但因為 dispatch
本身是讓 Redux 保證不會更新,因此可以放心放入。
因為該 Thunk 本身的目的,是在畫面讀取完後獲取資料,因此最合適在整個 App 處理完後,才進行下載的元件為 App.js。
因此上述的 useEffect
程式碼,是放在 App.js 裡。確認後,在 App.js 讀取完後,就會執行 fetchCartDataThunk
的 Thunk 函式,並在更新後同步到元件中。
完整程式碼範例
// IN cartActions.js
export const fetchCartDataThunk = createAsyncThunk(
"cart/fetchCartData",
async (_, { dispatch }) => {
try {
const response = await fetch(URL);
if (!response.ok) throw new Error("Fetch cart data failed.");
const data = await response.json();
return data;
} catch (error) {
console.error(error);
dispatch(notifyFor(messages.error));
}
}
);
// IN cartSlice.js
const cartSlice = createSlice({
name: "cart",
initialState: initialState,
reducers: [],
extraReducers: (builder) => {
builder
.addCase(fetchCartDataThunk.fulfilled, (state, action) => {
state.cartItems = action.payload?.cartItems || [];
state.cartQuantity = action.payload?.cartQuantity || 0;
})
.addCase(fetchCartDataThunk.rejected, (state, action) => {
state.status = "error";
state.error = action.error;
});
},
});
// IN App.js
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCartDataThunk());
}, [dispatch]);
createAsyncThunk
整體的使用,是不是其實比想像中簡單呢!
需要知道 Thunk 所使用的 payloadCreator
總是會返回 fulfilled 狀態的 Promise,因為他們不希望在開發者沒有 catch error 的情況下,就跳出 Uncaught Error。
因此 Thunk 會返回一個 fulfilled 的 Promise,並在該 Promise 中儲存 Fulfilled 或 Rejected 的資料,並讓我們獲取這些資料進行處理。
所以如果直接使用 then 來串接後續的行為,則永遠只會收到 fulfilled 的 Promise 而已。
當我們希望直接在元件中獲取資料,而不希望隔一層從 Redux 的 store
獲取錯誤資料,這時就可以使用 unwrap
或 unwrapResult
,用來進一步處理後續的行動。使用的方式有兩個,分別是:
unwrap
:會直接解打包 dispatch 回傳的資料,並像一般的 Promise 進行處理。unwrapResult
:相同目的,但以函式傳入 dispatch 後的返回值。那如果中間有錯誤,Promise 要回傳的錯誤是怎麼來的呢?其實是我們需要從 ThunkAPI.rejectWithValue(value, [meta])
特別設定傳入,不然 Redux 就會自動衍生一個序列化錯誤(Serialized Error)。
除此之外,還有更多的 API 可以使用,可以自行參考官方文件 的內容。
createAsyncThunk
除了能夠讓我們自動派發不同 Promise 的狀態,他也自動幫我們加入了其他三個 middleware 處理:
具體的返回值,在 configureStore
裡,有個參數是:middleware,此處可以手動設定所有的 middleware。
一旦設定該屬性,Redux 就只會讀取該陣列裡的 middleware。若要載入預設的 middleware,則可使用 getDefaultMiddleware()
來獲取。
const cartSlice = createSlice({
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});
// getDefaultMiddleware will return as follow
const middleware = [
actionCreatorInvariant,
immutableStateInvariant,
thunk,
serializableStateInvariant,
];
這個案例,透過 PUT 方式,來將購物車(Cart)的資料上傳到資料庫裡。整個案例分成兩個階段:
並且我們為了讓畫面的 UI 可以根據異步狀態更新,因此使用了 ThunkAPI 來派發另一個負責處理 UI 的 Action 來派發任務。
在這個階段,我們設定了 Type 為 cart/sendCartData
,並在 payloadCreator
傳入 cart
作為 arg
;並從 ThunkAPI
抽取出 { dispatch }
的 API 來派發任務。
整個上傳的過程,分成三個階段:
接著在 App 元件裡,在 useEffect 中透過 dependencies
,追蹤 cart 資料的更動。其中,為了讓第一次執行時,不要讓空白的 cart,直接上傳取代原本儲存的資料。故使用 isInitial
的變數,在第一次時就直接 return 跳出,後續才會上傳更新 cart 資料。
完整程式碼範例
// IN cartActions.js
export const sendCartDataThunk = createAsyncThunk(
"cart/sendCartData",
async (cart, { dispatch }) => {
try {
dispatch(uiActions.showNotification("Sending..."));
await fetch(URL, {
method: "PUT",
body: JSON.stringify({
cartItems: cart.cartItems,
cartQuantity: cart.cartQuantity,
}),
});
dispatch(uiActions.showNotification("Success!"));
} catch (error) {
dispatch(uiActions.showNotification("Error"));
console.log(error);
}
}
);
// IN App.js
let isInitial = true;
useEffect(() => {
if (isInitial) {
isInitial = false;
return;
}
if (cart.isChanged) {
dispatch(sendCartDataThunk(cart));
}
}, [cart, dispatch]);
在 Redux 裡面除了基礎的 state 更新外,算是非常複雜的功能和概念,也因此有了這一篇文章。createAsyncThunk
在異步資料的處理上,提供了完整且全面的設計與開發體驗。
希望讀者在使用 createAsyncThunk
時,可以少走一些彎路,並真正感受到 Thunk 帶來的便利與優勢。