はじめに
(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の仕組みを利用しているのかというと、主にはactions
、 action creators
、 components
が「直接的に」データに(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
だけを使うとできない。
redux
がthunk
を処理できるようにするのがredux-thunk
なのです。
Redux-Thunk Middleware
redux-thunkがインストールされると、Dispatchはこのようのものに変えられます:
actionOrThunk =>
typeof actionOrThunk === 'function'
? actionOrThunk(dispatch, getState)
: passAlong(actionOrThunk);
ようするに:
- Dispatchされたのは一般の
action object
だと、そのまま通す。 - Dispatchされたのは関数(すなわちthunk)になると、その関数に
store
のdispatch
とgetState
を渡して実行させる。
あとは、Tight Couplingの問題が残っていますが、それもredux-thunk
がDIを通じて解決しました。
普通のthunkは引数を取らないが、redux-thunk
はその型を破って、接続したstore
のdispatch
とgetState
をthunkに渡している。
すなわち、action creators
はstore
を指定してない、dispatch
だけもらってそのままを使う。
ここでもしredux-thunk
が接続したのはmockしたstore
であれば、生成されたactions
はmockしたstore
にdispatch
されます。
これで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
の基礎を理解した…気がしなくてもない。
本来の文章では他の内容ものってるので、よければそちらも参照してみてはいかがでしょうか。
何か間違ってる部分や不自然な日本語がありましたら、コメントを頂けると嬉しいです。