#はじめに
前回Reduxについての記事[[1]]を書きましたが、非同期処理についてはあまり触れてきませんでした。実務で使用する必要が出てきたのでRedux Thunkについてまとめていこうと思います。
#前提
Reduxが使える
Reactが使える
TypeScriptが使える
非同期処理が何か分かる
#Redux Thunk
Reduxにおける非同期処理のためのミドルウェアです。Redux Style Guide優先度Cのルールにも非同期ロジックにRedux Thunkを使うということが書かれている公式推奨の非同期用ミドルウェアです(賛否両論ありますが)。
#Redux Thunkの概念
これら記事[[2]][[3]]を参考に解説をしていきます。まず、普通に非同期処理の実装を考える場合を考えます。その場合コンポーネントまたはhooksで非同期処理を行い、actionをdispatchをするという流れになります。それに対し、非同期処理もまとめてactionで処理をしたいと考えた場合問題が発生します。Reduxにおいてdispatchするactionはtypeとpayloadを持つオブジェクトでなければいけません。しかし、async関数でAction Creatorを実装した場合、Promiseオブジェクトが返されてしまうため、actionに非同期処理を実装出来ません。
それを解決するミドルウェアがRedux Thunkです。Thunkというのは関数を使用して遅延を行う概念です。下に概念的なThunkの例を載せておきます。
let thunk = () => 1 + 2
// この時点では 1 + 2 は評価されない
thunk()
// 3
// Thunk関数が呼び出されたタイミングで 1 + 2 が評価される
[[3]]より引用
要するにThunkというのは遅延のための関数であり、Redux ThunkはこのThunkの概念をReduxで利用出来るようにするミドルウェアです。Redux Thunkを導入することでdispatchにactionだけでなく、非同期関数も渡すことが出来ます。**つまり、dipatchされた時の動作は2パターンあります。actionがdispatchされた場合は通常通りにreducerの実行します。関数がdispatchされた場合はRedux Thunkで処理が行われ、Redux Thunkからactionがdispatchされます。**下記にReduxの非同期処理のデータフローを載せておきます。
ReduxにおいてThunk関数は同期、非同期を問わず、任意のロジックを含むことが出来、いつでもdispatchやgetStateを呼び出すことが出来ます。また、引数としてRedux StoreのdispatchメソッドとgetStateメソッドの2つの引数を受け取れます。またThunk関数はactionともみなせるので、ここからはThunk Actionと呼んでいくことにします。
Thunk Actionはdispatchとstateを引数に持つ関数であるので、Thunk Actionを返すAction Creatorは関数を返します。Thunk Action内に非同期処理を実装することで、Redux内で非同期処理を扱うことが出来ます。Thunk Actionの型定義では4つの引数が存在し、第一引数がactionの戻り値の型、第二引数がstateの型、第三引数がThunk Actionのdispatch・getState以外の引数の型、第四引数がactionの型となっています。下記に例を載せておきます。
export const getUsers = (): ThunkAction<
void,
RootState,
unknown,
AnyAction
> => {
return async (dispatch: AppThunkDispatch) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data: userDataType[] = await res.json();
dispatch(setUsers(data));
};
};
#実際のコードで確認する
JSONSPlaceholderを利用してユーザーデータを取ってくるAPI処理を考えます。
###1 storeにミドルウェアの追加を行う
これはやらなくても動いたのですが、Redux ToolKitを導入している影響なのかなぜ動くのかよく分かりません。調べた感じ要るんじゃないかなあと思っているんですが、どなたか有識者教えてください。
export const store = configureStore({
reducer: {
・・・
},
middleware: [thunk],
});
###2 Thunk Action Creatorの作成
userDataというstateを作成し、データをフェッチしてきてstateにセットするというThunk Actionを返すThunk Action Creatorを作成しています。あとなぜか、stateにpayloadを直接突っ込むと再レンダリングされませんでした。調べた感じimmutabilityの問題っぽいのですが、reducerの中は内部的にimmerがあるので問題ないはずなのになんでなのか分かりません。Thunk Action内ではimmerが効かないとかなんでしょうか。とりあえずreturnすることで動作しましたが、後で要検証です。
import { AnyAction } from "redux";
import { createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit";
import { AppThunkDispatch, RootState } from "../../assets/type/reduxType";
import { useAppSelector } from "../../hooks/useAppSelector";
export type userDataType = {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
};
const initialState: userDataType[] = [];
export const userDataSlice = createSlice({
name: "userData",
initialState: initialState,
reducers: {
setUsers: (state, action: PayloadAction<userDataType[]>) => {
// state = action.payload; だめ?
return action.payload;
},
},
});
export const getUsers = (): ThunkAction<
void,
RootState,
unknown,
AnyAction
> => {
return async (dispatch: AppThunkDispatch) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data: userDataType[] = await res.json();
dispatch(setUsers(data));
};
};
//action
export const { setUsers } = userDataSlice.actions;
//selector
export const useUserDataSelector = () => {
const userData = useAppSelector((state: RootState) => state.userData);
return { userData };
};
//reducer
export const userDataReducer = userDataSlice.reducer;
###3 useDispatchの型変更
dispatchをthunkにも対応するように型変更しないとエラーが出ます[[5]]。
export type AppThunkDispatch = ThunkDispatch<RootState, void, AnyAction>;
import { AppThunkDispatch } from "../assets/type/reduxType";
import { useDispatch } from "react-redux";
export const useAppDispatch = () => {
const dispatch = useDispatch<AppThunkDispatch>();
return { dispatch };
};
###4 コンポーネントでのThunk Action Creatorの呼び出し
コンポネート側の記述はこのようになります。
import { VFC } from "react";
import { useAppDispatch } from "../../hooks/useAppDispatch";
import {
getUsers,
setUsers,
useUserDataSelector,
} from "../../store/slices/userData";
export const ReduxThunkPage1: VFC = () => {
const { userData } = useUserDataSelector();
const { dispatch } = useAppDispatch();
return (
<div>
<button onClick={() => dispatch(getUsers())}>フェッチデータ</button>
<button onClick={() => dispatch(setUsers([]))}>データクリア</button>
{userData &&
userData.map((user, index) => <div key={index}>{user.name}</div>)}
</div>
);
};
#おわりに
やっている事自体はそんなに難しくなかったのですが、公式ドキュメントが読みづらすぎて理解するのに時間がかかってしまいました。Redux ToolKitを使用している場合はcreateAsyncThunkを使用すれば良いのかなと思うのですが、既存プロジェクトだとRedux Toolkitを使っていないものも多いので、Redux Thunkと出会う機会は少なくないでしょう。今度はRecoilでReduxを代替出来ないかについて考えていきたいと思います。
#参考文献
[1]:https://qiita.com/it_tsumugi/items/f6efefe8757fd0fa00d8
[2]:https://redux.js.org/usage/writing-logic-thunks
[3]:https://nishinatoshiharu.com/redux-thunk-axios-tutorial/#TodoRedux_Thunk-2
[4]:https://redux.js.org/tutorials/fundamentals/part-6-async-logic#redux-async-data-flow
[5]:https://redux.js.org/usage/usage-with-typescript#typing-additional-redux-logic
[6]:https://tech.frenps.co.jp/archives/742
[7]:https://reffect.co.jp/react/redux-toolkit#Store
[[1]]:今から始めるRedux x React x TypeScript
[[2]]:Writing Logic with Thunks
[[3]]:Todoアプリで理解するRedux Thunkによる非同期処理の実装方法
[[4]]:Redux Async Data Flow
[[5]]:Typing Additional Redux Logic
[[6]]:typescriptでuseDispatchでRedux Thunkのthenが型エラーになる時の対応
[[7]]:Redux入門者向け初めてのRedux ToolkitとRedux Thunkの非同期処理