こんにちは HRBrainでフロントエンドを書いている鈴木です
この記事はHRBrainAdventCalendar8日目の記事です
Reduxには @reduxjs/toolkit
という超メガドデカハチャメチャ便利ライブラリがありますが、 createAsyncThunk
で作ったコードでなんやかんやしようとするとかなりハマったのでそちらの紹介です
redux-toolkit.js.org
型パラしんどい
createAsyncThunk
には3つの型パラメーターが必要ですが、複雑でかなりしんどいです
まずは型定義を見てみましょう
function createAsyncThunk< Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {} >( typePrefix: string, payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>, options?: AsyncThunkOptions<ThunkArg, ThunkApiCongi> ): AsyncThunk<Returned, ThunkArg, ThunkApiConfig>;
パッと見よくわかりませんね 一つ一つ見ていきましょう
結論
function createAsyncThunk< 第2引数の関数の返り値, 第2引数の関数の第1引数の型(生成された関数を実行する時に必要な引数), Thunkが引き回しているコンテキストの型 >(/* ... */)
以下解説です
Returned
1つ目の型パラメータは Returned
となっています
この Returned
は AsyncThunkPayloadCreator
に渡されているので、こっちを見てみます
type AsyncThunkPayloadCreator< Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {} > = (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => AsyncThunkPayloadCreatorReturnValue<Returned, ThunkApiConfig>;
AsyncThunkPayloadCreator
から渡ってきた Returned
は更に AsyncThunkPayloadCreatorReturnValue
に渡されています
type AsyncThunkPayloadCreatorReturnValue< Returned, ThunkApiConfig extends AsyncThunkConfig > = Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>> | Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>;
入ってきた Returned
がそのまま Promise<T>
の中に渡されています
そもそも AsyncThunkPayloadCreatorReturnValue
は AsyncThunkPayloadCreator
の返り値として定義されていましたし、更に AsyncThunkPayloadCreator
は createAsyncThunk
の第2引数の関数だと定義されていたので…
どうやら createAsyncThunkの第2引数の関数の返り値 だということがわかりました
ですので、大抵は void
とかフェッチしてきたデータを返してあげたりする時の型を入れてあげれば良いでしょう
// 例えばこんなかんじ const fetch = createAsyncThunk<{ users: User[] }, ?, ?>('fetch', async (arg, thunkAPI) => { const res = await axios.get<User[]>('/api/users') return res.data })
ThunkArg
次は ThunkArg
ですが、こちらも AsyncThunkPayloadCreator
に渡されてたのでもう一度思い出してみます
type AsyncThunkPayloadCreator< Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {} > = (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => AsyncThunkPayloadCreatorReturnValue<Returned, ThunkApiConfig>;
こちらはこの時点で第1引数の arg
で使われています
ということは、 createAsyncThunkの第2引数の関数の第1引数 であることがわかりました
ThunkApiConfig
これはRedux ToolkitがThunkのAPIをまとめた型で、中にはおなじみの dispatch
や getState
などが入っています
ThunkApiConfig extends AsyncThunkConfig
となっているので、 AsyncThunkConfig
を詳しく見てみましょう
type AsyncThunkConfig = { state?: unknown; dispatch?: Dispatch; extra?: unknown; rejectValue?: unknown; }
とんでもなくざっくりとした型となっています これを実際のコードのどこで使うかと言うと、第2引数の関数の第2引数で使います
const hoge = createAsyncThunk('hoge', (arg, thunkAPI) => {}) // ^^^^^^^^^ これ
thunkAPI
の名前にある通り redux-thunk
で使う関数などが入っています
型がざっくりしているのは、開発プロジェクトによって可変になるからです
このままだと使いづらいし何よりRedux Toolkitの型定義がこいつを export
してくれていないので、自分の好きなように型を作ってしまいます
type RootState = { /* ... */ } type ExtraArg = { /* ... */ } export type AsyncThunkConfig<T = unknown> = { state: RootState dispatch: Dispatch extra: ExtraArg rejectValue: T }
使い方
全部の型パラメータの正体がわかったので早速使ってみましょう
// 例 カウンターの増幅はこうなりそう const increment = createAsyncThunk< { count: number }, undefined, AsyncThunkConfig >('increment', (arg, thunkAPI) => { return thunkAPI.getState().count + 1 }) // 例 IDからユーザーをフェッチするとこう const fetchUserById = createAsyncThunk< { user: User }, { id: string }, AsyncThunkConfig >('fetchUserById', async (arg, thunkAPI) => { const res = await window.fetch(`/api/user/${arg.id}`) return JSON.parse(res.json()) })
動きがいまいちわからん
さて、これだけ丹精込めて createAsyncThunk
を使って関数を作っても、肝心の作った関数の使い方が全然わからないということはないでしょうか
実は従来の redux-thunk
のように使うよりもより便利なAPIがReduxToolkitから提供されていることも併せて理解しておきたいところです
作った関数を実行する
これは特に変なところはないです 普通に実行しましょう
const increment = createAsyncThunk(/* ... */) const Component = () => { const dispatch = ReactRedux.useDispatch() const clickIncrement = React.useCallback(() => { // 普通にThunk関数として使える dispatch(increment()) }, [dispatch]) return (/* ... */) }
非同期のアクションを受け取る
createAsyncThunk
で作成したThunk関数には必ず pending
fulfilled
rejected
の3つのActionが一緒に作成されます
どれも作成されたThunk関数の中にオブジェクトとして入っており、以下のように参照可能です
const increment = createAsyncThunk(/* ... */) increment.pending increment.fulfilled increment.rejected
また、それぞれが表すイベントはこんな感じです
Action | 効果 |
---|---|
pending | 実行中 Promise.resolve()が呼ばれる前 |
fulfilled | 正常終了 Promise.resolve() が呼ばれた時 |
rejected | 異常終了 Promise.reject() が呼ばれた時 |
これらを使うと、非同期関数を実行した時の各イベントでReducerを書くことができます
const slice = createSlice({ name: 'counter', reducer: {}, extraReducer: builder => { builder.addCase(increment.pending, (state, action) => { /* ... */ }) builder.addCase(increment.fulfilled, (state, action) => { /* ... */ }) builder.addCase(increment.rejected, (state, action) => { /* ... */ }) } })
めちゃくちゃ便利ですね フェッチの関数とかであればフェッチ中はローディングのフラグをStateに建てるなんてこともできちゃいます
更に、 fulfilled
と rejected
では action.payload
に値を詰めることができます
まずは createAsyncThunk
の中身を弄ります
const fetch = createAsyncThunk< // Returned = fulfilledのPayload { users: User[] }, undefined, // AsyncThunkConfig<T> の T = rejectedのPayload AsyncThunkConfig<{ errorMessage: string }> >( 'fetch', (arg, thunkAPI) => { let res try { res = await fetch('/api/users') } catch (e) { thunkAPI.rejectWithValue({ errorMessage: 'Fetch error' }) return } return JSON.parse(res.json()) } )
fulfilled
に値を詰める場合は createAsyncThunk
の Returned
が型になり、第2引数の関数の返り値がそのまま入ります
rejected
に値を詰める場合は AsyncThunkConfig<T>
の { rejectValue: T }
がその型になります 値を詰めるのは thunkAPI.rejectWithValue()
で行います
そうしたらあとはReducerで受け取るだけです
const slice = createSlice({ name: 'user', reducer: {}, extraReducer: builder => { builder.addCase(increment.fulfilled, (state, action) => { // { users: User[] } が入ってくる state.list = action.payload.users }) builder.addCase(increment.rejected, (state, action) => { state.status = 'error' // { errorMessage: string } が入ってくる state.message = action.payload.errorMessage }) } })
このように非同期のActionを使いこなすことによって 非同期の管理からエラー処理までを簡単に書くことができます
おわりに
ReduxToolkit の AsyncThunk は非常に強力です!この機会に是非使いこなしてみてください!