122
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

redux-thunk入門、簡単まとめ

Last updated at Posted at 2019-08-20

はじめに

(2020.4.23 簡単な例を追加、Tight Coupling関連の記述を修正しました。)

昔ReactとReduxを触ったこと全然ないので、インターンはじめて以来、ただ「こういう風に書くもの」、「このように書くのは正しい」としか認識していない状況でした。そもそもどの部分が redux-thunk なのかも全然分かってません。
なので、今回は redux-thunk 公式に推奨されてる文章を読んで、自分なりに理解した後、記録してみます。
全文を翻訳するわけではないので、詳細はそちらに参照していただければと思います。
ライブラリーは何故作られたのか、どういう問題を解決したのか、それを理解するのはやはり大事なことだと思います。

Thunkとは

一般的にいうと、functional programmingのテックニックの一つで、そのまま関数Aを利用するのではなく、まず関数Bに変数を提供して、関数Bはそれを使って関数Aの中身を完成させる。最後は完成した関数Aを返す、必要な時に関数Aを呼び出す感じになってます。
コードだとこういう感じになります。

function yell (text) {
  console.log(text + '!')
}

yell('bonjour') // 'bonjour!'

function thunkedYell (text) {
  return function thunk () {
    console.log(text + '!')
  }
}

const thunk = thunkedYell('bonjour') // まだ実行されてない

thunk() // 'bonjour!' //必要時に呼ぶ

で、React / Redux はどういう風にThunkの仕組みを利用しているのかというと、主にはactionsaction creatorscomponentsが「直接的に」データに(Asyncなどによる)影響を起こさせないようにしています。
それらの処理は全部Thunkに包んで、そのあとmiddlewareがThunk呼ぶ時に実行されます。
このようの仕組みだと、少なくともMiddlewareレベル以外のところは比較的にピュア(Async関連の処理をしない)になるので、メンテナンス、テスト、読みやすさでは役に立ちます。

Redux-thunkが作られた理由

reduxだけ利用すると発生する問題

Reduxのdispatchの引数はaction object。

const LOGIN = 'LOGIN'
store.dispatch({ type: LOGIN, user: {name: 'Lady GaGa'} })

で、Asyncのリクエストが組み込まれた場合は:(Axiosを例としている)


const asyncLogin = () =>
  axios.get('/api/auth/me')
  .then(res => res.data)
  .then(user => {
    // 疑問:このuserはどうやって利用する?
    // ここで直接Storeに送るか?
  })

// それに、componentのどこかで:
store.dispatch(asyncLogin()) // こういう風にもできない; `asyncLogin()` は promise、actionではない

解決法

一見では、async handlerの中で store.dispatch 呼ぶことで確かに解決はできますが:

import store from '../store'

const simpleLogin = user => ({ type: LOGIN, user })

const asyncLogin = () =>
  axios.get('/api/auth/me')
  .then(res => res.data)
  .then(user => {
    //直接Storeに送るか!
    store.dispatch(simpleLogin(user))
  })

// で、componentのどこかで:
asyncLogin()

さて、これだといくつかの問題があります。

問題1:Inconsistent API

このように書くと、components の中の処理は二種類に別れることになります。
一つはstore.dispatch(syncActionCreator())のように、dispatchを呼ぶ処理と、
doSomeAsyncThing()のような処理。
それでは一致性を失うことになります。

後者の処理ではdispatch関連の部分がどう処理されてるのかのを理解しにくい。
それに、もし処理をsyncからasyncに変更する場合(逆も然り)は、componentの中の関連記述も修正しなければならないので、メンテナンス上では非常に手間がかかります。

問題2:Impurity

前述したasyncLoginは明らかにピュアな関数ではありません。それ自体はまあ仕方がないが、このようにcomponentが直接にデータに影響を起こすと、一見では悟りにくいので、メンテナンス上でも、unit testing上(例えばaxiosをmockする)でもよしとされません。

問題3:Tight Coupling

asyncLogin(action creator)の利用は一つ特定のRedux storeに限定されています。
(たとえばテストする時にMockしたい)

Thunkでの解決法を試みる

Thunkを使って、network関連の処理は「直ちに」実行させない、
その代わりにThunkの中に包んで、Thunkを返す。

import store from '../store'   // 今はバインドしているのでTight Coupling

const simpleLogin = user => ({ type: LOGIN, user })

const thunkedLogin = () =>    
  () =>                        
    axios.get('/api/auth/me')
    .then(res => res.data)
    .then(user => {
      store.dispatch(simpleLogin(user))
    })

// componentのどこかで:
store.dispatch(thunkedLogin()) // thunk自体をStoreに送…れるのか?

これでは、問題1のdoSomeAsyncThing()componentの中に存在しなくなります、thunkedLoginも比較的にピュアになります(実行される時にThunkを返すだけなので)。

でも待って、action creatorが返すのはaction objectではないので、reduxは処理できないはずでは?

そう、本来のreduxだけを使うとできない。
reduxthunkを処理できるようにするのがredux-thunkなのです。

Redux-Thunk Middleware

redux-thunkがインストールされると、Dispatchはこのようのものに変えられます:

actionOrThunk =>
  typeof actionOrThunk === 'function'
    ? actionOrThunk(dispatch, getState)
    : passAlong(actionOrThunk);

ようするに:

  • Dispatchされたのは一般のaction objectだと、そのまま通す。
  • Dispatchされたのは関数(すなわちthunk)になると、その関数にstoredispatchgetStateを渡して実行させる。

あとは、Tight Couplingの問題が残っていますが、それもredux-thunkがDIを通じて解決しました。
普通のthunkは引数を取らないが、redux-thunkはその型を破って、接続したstoredispatchgetStateをthunkに渡している。
すなわち、action creatorsstoreを指定してない、dispatchだけもらってそのままを使う。
ここでもしredux-thunkが接続したのはmockしたstoreであれば、生成されたactionsはmockしたstoredispatchされます。
これでstoreを簡単にに変えられますね。

Redux-Thunkを使った例

actions.ts

import axios from 'axios'
import * as actionTypes from './actionTypes'

export function fetchAll(): (dispatch: any) => any {
  return dispatch => {
    dispatch({ type: actionTypes.FETCH_USERS})
    axios.get('/api/users').then(response => {
      dispatch({
        type: actionTypes.FETCH_USERS_SUCCESS,
        payload: response.data,
      })
    })
  }
}

TestPage.tsx


import { fetchAll } from 'actions'
import { useDispatch } from 'react-redux'

export const TestPage: React.FC = () => {
  const dispatch = useDispatch()
  return (<button onClick={() => dispatch(fetchAll())}>Fetch</button>)
}

終わりに

何回も読みながら情報整理した後、ようやくredux-thunkの基礎を理解した…気がしなくてもない。
本来の文章では他の内容ものってるので、よければそちらも参照してみてはいかがでしょうか。
何か間違ってる部分や不自然な日本語がありましたら、コメントを頂けると嬉しいです。

122
80
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
122
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?