ThunkActionとは
Reduxで非同期処理を書くための仕組みを指します。
Reduxの世界では、Reducerは「同期的」「純粋関数」でなければならないため、
API呼び出しやsetTimeoutなどの副作用(非同期処理)はreducer内に書けません。
上記のような処理を書くために登場するのがThunkActionです。
Thunkという単語は元々「ある処理を遅延させて後から実行できるようにした関数」を指します。
ThunkActionは何ができるの?
Reduxで扱う通常のActionはオブジェクトですが、
Thunk を使うと Actionの代わりに関数をdispatchできるようになります。
function fetchUser(userId) {
return async function (dispatch, getState) {
dispatch({ type: "user/loading" })
const user = await api.fetch(userId)
dispatch({ type: "user/loaded", payload: user })
}
}
これを dispatch(fetchUser(1)) のように実行すると、
内部では以下が行われます:
- dispatch({ type: "loading" }) → ローディング UI へ
- API などの非同期処理を実行
- dispatch({ type: "loaded", payload }) → 結果を state に格納
つまり ThunkAction は、
Action を「関数化」することで、非同期処理+複数の dispatch をまとめた処理を書けるようになります。
RTKでの使い方
ReduxToolKitでThunkActionを利用するにはcreateAsyncThunkを使うのが一般的とされています
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// ThunkActionを定義。ユーザーをIDで取得する副作用を持つ。
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
)
interface UsersState {
entities: User[]
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState = {
entities: [],
loading: 'idle',
} satisfies UserState as UsersState
// スライスの定義
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
・・・
},
extraReducers: (builder) => {
// ThunkActionをReducerに登録
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
},
})
// ThunkAction呼び出し
dispatch(fetchUserById(123))
ThunkActionのメリット・デメリット
メリット
- Reduxに副作用を安全に持ち込める
- dispatchが複数回できる
- getStateで現在のstateを参照して判断できる
非同期処理をActionとして扱える
デメリット
- 手書きするとコードが長くなる
- 型定義が複雑(ThunkAction 型)
- 複雑な処理がどんどん Thunk の中に肥大化しやすい(createAsyncThunk利用推奨)
おわりに
最後まで読んでいただきありがとうございました!
ThunkActionについて学ぶ際に、Reducer は「同期的」「純粋関数」でなければならないというのも副次的な学びになりました!