September 01, 2019
状態管理に redux を使っている人は以下の様なことをやりたいことがあるでしょう。
このような非同期処理の結果を redux に保持することが必要な時にはよく redux-thunk
, または redux-saga
が採用されていた。
今回は非同期処理に thunk も saga も使う必要なくなったのでは?と言う話です。
なお、筆者は redux-saga についてはそこまで使ったことが無いので、この記事では主にredux-thunk
のリプレイスについて書きます。
redux-thunk で非同期処理の流れを簡単に書くと:
となります。
コードで書くとこういう感じです:
fooReducer.ts
type FooState = {loadng: boolean,list: Item[],error: stirng}const initialState:FooState = {loading: false,list: [],error: string}const fooReducer = (state:FooState = initialState, action:FooAction) => {switch(action.type) {case: 'FOO_START':return { ...state, loading: true}case: 'FOO_SUCCESS':return { ...state, loading: false, list: action.result}case: 'FOO_FAILED':return { ...state, loading: false, error: action.error}default:return state;}}
fooAction.ts
export const fooAction = (): ThunkAction => async (dispatch: Dispatch) => {// 非同期処理を開始するため、状態をLoadingにするdispatch({ type: 'FOO_START' })// 非同期処理を行うtry {const result = await fetch('/getFoo')// 成功したら結果をreduxに反映し、Loading状態を外すdispatch({ type: 'FOO_SUCCESS', result })} catch (e) {// エラーが発生したらエラーメッセージを表示させ、Loading状態を外すconsole.error(e)dispatch({ type: 'FOO_FAILED', error: e.message })}}
上記の通り、redux-thunk がやっていること自体は単純なのですが、 それを書くのに結構な量のコードを書く必要があります。 上記に加えて、store の状態をもとに処理を分岐させる必要がある場合、thunk 内で getState を呼んだりと、処理が複雑化してきます。
筆者が感じる Issue としては以下のようなことがあります:
STARTED
, SUCCESS
, FAILED
の 3 つの Action を書く必要があった。Loading
の状態を reducer で持つ必要があった。つまりやりたいことは:
react の custom hooks を使えば解決します。 react-redux の v7.1 から、hooks に対応した API が出たのでそれらを使っていきます。
上で書いた redux-thunk の例を custom hooks を使って簡略化することができます。
以下のことをやっていきます。
useFoo.ts
import { useState, useCallback } form 'react'import { useSelector, useDispatch } from 'react-redux'import { State, FooAction } from './types'const useFoo = () => {const [loading, setLoading] = useState(false)const [error, setError] = useState('')const items = useSelector((state:State)=> state.foo.items)const dispatch = useDispatch<Dispatch<FooAction>>()const getFoo = useCallback(async () => {setLoading(true)try {const result = await fetch('/getFoo')setLoading(false)dispatch({type: 'FOO_SUCCESS', result})} catch (e) {setLoading(false)setError(e.message)}},[loading, error, items])return [items, getFoo, loading, error]}
loading と error を持つ必要がなくなったので、reducer も簡略化することができます。
fooReducer.ts
type FooState = {list: Item[],}const initialState:FooState = {list: [],}const fooReducer = (state:FooState = initialState, action:FooAction) => {switch(action.type) {case: 'FOO_SUCCESS':return { ...state, list: action.result}default:return state;}}
component での使用例はこんな感じです
FooList.tsx
import React, { useEffect } from 'react'import { useFoo } from './useFoo'const FooList = () => {const [items, getFoo, loading, error] = useFoo()useEffect(() => {getFoo()}, [])if (loading) {return <p>読込中…</p>}return (<div>{error ? <p>{error}</p> : null}<ul>{items.map(item => {return <li>{item.contents}</li>})}</ul></div>)}
このような形で、ページ遷移時に非同期で何かを取得してくる処理を thunk 無しでも実現することができます。
また、このように非同期処理のロジックを custom hooks に閉じ込めておくことで、別のページで同じ処理が必要になった時に hooks を使い回すことができます 上の Compnent の例では、useEffect も useFoo の中に入れることで、完全にロジックと Component を分割させることができます。
これはあくまで自分の redux-thunk の使い方なら redux-thunk 使わなくても出来るかなと言う話なので、 redux-thunk や redux-saga じゃないとこれができないよ!みたいなことがあれば教えてほしいです。