読むだけRedux
なんだかんだでちょっとしたツールでもGUIがあると便利だなぁと思ったりします. かといってOSネイティブなのはちょっとと思うとWebフロントエンドににバックエンドとなるわけです. 1 つまりSPAなのですが, Vue.js, Angular, Reactを昔にドキュメント読んで触ったわけですが, 結局Reactかなぁと思ってReduxのコードを読んでいたんですがContext APIで十分な気がしてきたりしました. すでにこの辺の記事は大量にあるわけですが, せっかくなので勉強したことを書いておこうと思ったわけです.
reduxとreact-redux
Reduxといってもパッケージは二つに別れてたりします.
redux | react-redux |
---|---|
reduxのストアなど | reactとreduxをつなぐ |
つまりパッケージとしてはreduxとreact-reduxは別ものであるということを認識しておく必要があります. Reduxを単体で使うこともできるようです.
createStoreはクロージャである.
Reduxの中心はStoreと呼ばれるものです. createStore関数で作られます.
const preloadedState = [];
const store = createStore(reducer, preloadedState);
このstoreが何かというのはソースに当たった方が分かりやすいです. 主要な部分を抜き出すと以下のようになります.
export default function createStore(store, preloadedState) {
// private variables
let currentReducer = reducer
let currentState = preloadedState
function dispatch(action) {
// update state here with reducer!!
}
// public functions
const store = ({
dispatch: dispatch
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
return store
}
公開されているのはstoreで定義されたオブジェクだけで, currentReducerとcurrentStateが内部の状態としてプライベートな変数となっているのがわかると思います. オブジェクト指向風にいうとインスタンスの内部状態を変更するためのメソッドをstoreとしてまとめて公開しているといことです. クラスとほぼ同じに見えてきたでしょうか?
dispatchでstateを更新する.
この中で重要なのはdispatchという関数です. やってることはとても単純です. dispatchを呼び出して内部状態のcurrentStateを更新しているだけです. その際最初に渡したreducerを使うわけです. reducerはactionに応じて処理を切り替えるswitchを関数化したものです.
function dispatch(action) {
// ・・・
try {
isDispatching = true
currentState = currentReducer(currentState, action) // *
} finally {
isDispatching = false
}
// ・・・
return action
}
Reduxと言われるものが担うのは実はおおよそこれだけです.2
Reduxの仕事
- stateを秘匿する
- dispatchでアクションを決める
- reducerで更新する
ReduxとReactの関係
さてこれでアプリケーション・ステートを管理するstoreというものが作れました. こいつをルート・コンポーネントからシャンパン・タワーのごとく注ぎ込めば万事オッケー・・・とはなりません
Storeから適切なstateとdispatchをReactのコンポーネントに渡してやらないといけません. これがreact-reduxパッケージが提供する役割です.
connect関数が返すのはHOCである.
両者を結びつけるには, 以下のような関数を定義してconnectという関数に渡してやります. その戻り値はまた関数でここには実際の表示を担うコンポーネントを渡してやります. これを定義しているのはSomeComponentという別のコンポーネントで, WrappedComponentにmapStateToPropsとmapDispatchToPropsで変換の方法を教えるだけのコンポーネントです.
const mapStateToProps((state, ownProps)) => {
valueName: state.active
})
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(someAction(args))
})
const wrapWithConnect = connect(mapStateToProps, mapDispatchToProps)
const Connect = wrapWithConnect(WrappedComponent)
// or
// const Connect = connect(mapStateToProps, mapDispatchToProps)(PresentationalComponent)
これで上位コンポーネントから渡されてきたstoreがSomeComponentに渡されるとmapStateToPropsとかmapDispatchToPropsの方法でWrappedComponentに渡されます. WrappedComponentではpropsを通じて,
- props.valueName
- props.onClick
という感じで特にstoreがどうとかstateだdispatchだと考えなくても通常のコンポーネントの時と同じように扱えるわけです. これは既存のコンポーネントの修正を少なくできると思います. ここでSomeComponentをContainerコンポーネントと呼び, WrappedComponentをPresentatioanlコンポーネントとか呼ぶそうです.
connect関数分からん3
さてreact-reduxはconnect関数が中心です. だからなんとなくconnect関数を呼べば全部やってくれるわけですが, 中は結構めんどくさい感じになっています.
- connect関数の戻り値はconnectAdvanced関数を実行した結果でwrapWithConnect関数を返す.4
- connectAdvanced関数の第1引数はselectorFactory
- selectorFactoryの正体はfinalPropsSelectorFactory
- selectorFactory関数を実行するとimpureFinalPropsSelectorFactory関数かpureFinalPropsSelectorFactory関数が実行され同名のimpureFinalPropsSelector/pureFinalPropsSelector関数が戻る.
この関係性を押さえた上で以下のような処理がconnectAdvanced関数で実行されているようです.
- connectAdvanced関数のヘルパー関数であるcreateChildSelector関数がstoreをもらいselectorFactoryを実行する.
- selectorFactoryはfinalPropsSelectorFactory関数の別名なのでimpureFinalPropsSelector/pureFinalPropsSelector関数が返ってくる.
- これらの関数がchildPropsSelectorとしてuseMemoでキャッシュされる.
- childPropsSelector関数を実行してpropsを取り出す. 多分actualChildPropsってやつ.
- actualChildPropsをWrappedComponentに渡す.
要はselectorFactory(finalPropsSelectorFactory)関数が上手やってくれているようです. 名前から何かを選択する関数が生成されることは予測できそうですが, それがprops選択関数で以下のいずれかになるわけです.
- impureFinalPropsSelector(mapStateToProps, mapDispatchToProps, mergeProps, dispatch)
- pureFinalPropsSelector(mapStateToProps, mapDispatchToProps, mergeProps, dispatch)
finalという言葉がポイントです. そしてこの関数に上述のmapStateToPropsとかmapDispatchToPropsとかを渡してpropsの具体的な選択方法を与えることになります. connectAdvanced関数のソースを意味が分かるように整理するとこんな感じでしょうか?
function createChildSelector(store) {
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
const childPropsSelector = createChildSelector(store)
const props = childPropsSelector(store.getState(), wrapperProps)
<WrappedComponent {...props} />
SomeComponentの正体
SomeComponentではconnect関数を使ってConnectコンポーネントを生成しました. これは何かという話を最後にしておきたいと思います. connectAdvanced関数内部ではHOC(Higher Order Component)というテクニックが使われて言います. 高階関数が関数を引数にしたり, 戻り値にしたりすることができる関数でした. 同様にHOCはコンポーネントを引数に取り, コンポーネントを返すことができるコンポーネントのようです.
wrapWithConnect関数がそれですが, この関数の内部ではConnectFunction関数コンポーネント持っており, これにWrappedComponent包みます. 最終的にConnectというコンポーネントを戻します. これがSomeComponentの正体になります. Reactにreduxを導入すると随分遠大なことをやっている様に感じるわけですが,5 基本的にはコンポーネントをRedux用のコンポーネントで包み込むということをやっているだけということになります.
まとめ
- stateはstoreの内部状態
- dispatchはstoreの公開関数
- stateの更新方法はreducerで教えてあげる
- stateからpropsへの変換方法はmapStateToPropsで教えてあげる
- dispatchからpropsへの変換方法はmapDispatchToPropsで教えてあげる
- storeからpropsに変換するコンポーネントがある
- connect関数分からん
- connectAdvanced関数分からん