始めに
Redux の store に対して非同期処理で action を dispatch するときには工夫が必要になります。redux-thunk を用いて実現する方法を今回まとめてみました。
誤り等ありましたらコメントでご指摘いただけますと助かります
redux-thunk とは
Redux のミドルウェアのひとつで、これを用いることで純粋なオブジェクトだけでなく関数も action として dispatch することができるようになります。
非同期ロジックを組み込んだ関数を store が解釈できるようになり、適切なタイミングで dispatch を行ってくれます。
コンポーネント側からしたら ActionCreator の戻り値を dispatch しておけば後は store がうまくやってくれるので、コンポーネントのロジックの見通しが良くなります。
簡単な実例
export const App = () => {
const tasks = useSelector(state => state.tasks);
const dispatch = useDispatch();
useEffect(() => {
// APIからfetchしたデータをもとにstoreにdispatch
dispatch(fetchTasks());
}, [])
return (
<ul>
{tasks.length && tasks.map(task => (
<li key={task.id}>{task.name}</li>
))}
</ul>
);
}
例えば、上のようなコンポーネント。
Redux とのつなぎ込みは Hooks を使っており、また useEffect から dispatch を呼んでおります。
ActionCreator を下のように実装したとします。
export const fetchTasks = () => {
fetch(api_path).then(response => {
return response.json();
}).then(tasks => {
return { type: 'set_tasks', tasks: tasks };
})
}
一見、action としてオブジェクトを return しているようにも見えますが、fetch API が同期的に返すのは Promise オブジェクトのため、dispatch が期待する結果にはなっていません。
下のエラーが発生します。
Actions must be plain objects. Use custom middleware for async actions.
意図通りにやるには、promise が resolve されタスクキューに入ったコールバック処理が実行されるまで待ち dispatch を行う必要があります。
例えば、下のように非同期処理の結果を dispatch する関数を設けて、コンポーネントでこの関数を呼ぶやり方。
const fetchTasks = () => {
fetch(api_path).then(response => {
return response.json();
}).then(tasks => {
dispatch({ type: 'set_tasks', tasks: tasks })
})
}
これでとりあえず動きはしますが、非同期処理と dispatch が混在していてコンポーネントの処理の見通しが悪くなってしまいます。また、dispatch 先の store に依存しているので mock でのテストなどがしずらそうです。
そこで、redux-thunk で Redux を拡張させ、非同期処理を含んだ関数をそのまま dispatch できるようにすることで処理をシンプルにしようとなります。
まずは、sotre 作成時にインポートした thunk を適用させます。
const store = createStore(reducer, applyMiddleware(thunk));
そしてActionCreator で、「dispatch を引数にとり、内部で(非同期処理後に) action をdispatch する関数」を返すようにします。
export const fetchTasks = () =>
dispatch => {
fetch(api_path).then(response => {
return response.json();
}).then(tasks => {
dispatch({ type: 'set_tasks', tasks: tasks })
})
}
こうすることでロジックを Redux に寄せることができますね。
あとは冒頭記載通りにコンポーネントから dispatch(fetchTask())
をしてこの関数ごと dispatch してやれば、store 側でうまく対応をしてくれます。
拡張された store での処理
store では、まずミドルウェアが渡ってきた action がオブジェクトか関数かの判定を行います。オブジェクトであった場合は通常通り reducer に渡します。関数であった場合は dispatch を引数に関数を実行し (非同期処理などを経て)action が dispatch されるまで待ちます。そして、ここで disptach された apction がオブジェクトであったら今度は reducer に渡す、という流れです。
(公式が貼っていたリンクの記事のアニメーションが分かりやすいです)
ちなみに、関数の第二引数として getState もとることができ、store の既存の state に関数内からアクセスして処理を分けることも可能です。
この流れを掴んだ上でソースを覗いてみると、この根幹の部分は意外とシンプルな実装であることが分かりますね。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
参考
https://github.com/reduxjs/redux-thunk
https://medium.com/fullstack-academy/thunks-in-redux-the-basics-85e538a3fe60
https://www.tohuandkonsome.site/entry/2019/02/05/231503
https://kde.hateblo.jp/entry/2019/02/14/220155