React+Reduxにも触れてみる 3. 試しに書いてみる
前提
前回こういうことをした。
だけど残念ながら完成形の説明だけをしていると筆者的に飽きてくるのでやはり試行錯誤を書くことにする。
ということで前回のことは忘れてほしい。
今回の話
まずは以下のpageの通りに環境をつくって動作確認してみる。
で、codeの読解をしてみた。そのときのlog抜粋。
読解
ActionとReducer
// Action名の定義 const SEND = 'SEND'; // Action Creators function send(value) { // Action return { type: SEND, value, }; }
まずはここ。まあActionはReduxにおける入り口だからいいんだけど、本当に関数定義しているだけなのでこれだけではなんともいえない。
ちなみに今でもなんとも言えない。とりあえずSENDというActionを定義した、という感触。そのActionがreturnするのはActionの種類であるSENDという名前と、valueである、という、見たままのもの。
function formReducer(state, action) { switch (action.type) { case 'SEND': return Object.assign({}, state, { value: action.value, }); default: return state; } }
Reducerが出てくると少しはわかる。action.typeを見てSENDだったらstateのvalueを更新している。で、stateが更新されたと言うことはrenderが実行されるということなので、次はviewが出てくる。
その前にObject.assignってなんだ。
ああ。hash型でstateに{value:action.value}をmergeしたものを生成しているのか。
で次はView。と思ったら先にStoreがきた。
/* Storeの実装 */ const initialState = { value: null, }; const store = createStore(formReducer, initialState);
ReducerとinitialStateを登録してstoreをconstで作成、と。
次がいよいよView。だけど何個かある。
記事が長くなってきたから次へ。これblogにできそうだな……。 (そして今している)
View部分
気を取り直してView
// View (Container Components) class FormApp extends React.Component { render() { return ( <div> <FormInput handleClick={this.props.onClick} /> <FormDisplay data={this.props.value} /> </div> ); } } FormApp.propTypes = { onClick: React.PropTypes.func.isRequired, value: React.PropTypes.string, }; // View (Presentational Components) class FormInput extends React.Component { send(e) { e.preventDefault(); this.props.handleClick(this.myInput.value.trim()); this.myInput.value = ''; return; } render() { return ( <form> <input type="text" ref={(ref) => (this.myInput = ref)} defaultValue="" /> <button onClick={(event) => this.send(event)}>Send</button> </form> ); } } FormInput.propTypes = { handleClick: React.PropTypes.func.isRequired, }; // View (Presentational Components) class FormDisplay extends React.Component { render() { return ( <div>{this.props.data}</div> ); } } FormDisplay.propTypes = { data: React.PropTypes.string, };
container componentsとあるけど
Container components: 機能に関するコンポーネント。いわゆるFluxでいうContainerです。主に直接Reduxと連携するコンポーネントで、ReduxのStoreの状態(state)を購読し、またReduxのActionをDispatchする役割を持ち、データを取得したり、stateの更新を行ったりします。主に親コンポーネントがこの役割を担います。 Presentational Components: 見た目に関するコンポーネント。Container componentsからpropsを通してデータを受け取り、Viewを構築します。また同様にpropsから受け取ったコールバックを実行します。
ああなるほど。Container components => Presentational ComponentsというTreeか。
で、FormAppはForm全体を定義するContainer component。この下にいろいろViewがつくことを考えれば、renderで定義されているformの内容もよくわかる。
refでthis.myInputというのを見ているけどどこで定義しているんだこれ。
ああ違う。this.myInputにinput domの中身を入れているだけだこれ。
buttonにもevent定義して、FormInput内で定義したsendを結びつけている。
それにしてもこういう風にmethod定義できるのはES6の文法な気がする。
正解。
propTypesはなんだろう。
それから、Propsを通して、ReduxのStoreと連携するようにしています。これは後ほど説明しますが、React-Reduxのconnect()()メソッドを使って実現しています。
ああReduxにstate管理を投げるための儀式っぽいな。じゃあskipで。
ここまで読み込むとあとは入れ子なのでそんなに迷わない。FormAppのrenderでFormInputとFormDisplayが呼び出され、それぞれで処理が定義されているだけ。
次はRedux部分かな。
Redux
// Connect to Redux function mapStateToProps(state) { return { value: state.value, }; } function mapDispatchToProps(dispatch) { return { onClick(value) { dispatch(send(value)); }, }; } const AppContainer = connect( mapStateToProps, mapDispatchToProps )(FormApp);
connectというのは最初に
import { Provider, connect } from 'react-redux';
で呼び出しているreactとreduxをつなぎ込んでいる。
これはReactのコンポーネントとなっており、このProviderの子コンポーネントに、上で作成したReactのContainer Component(この後説明するReact-Reduxのconnect()()メソッドでContainer化しておきます)を紐付けます。その際にstore属性を通して、Container ComponentにReduxの対象となるStoreを渡します。こうすることで、ReactとReduxが連携されます。
まあContainer Componentをreduxとつなぎ込むのだからconnectでFormAppを対象としているのは理解できる。 で、connectというのは何をしているのさ。Container化しているというけれど。……ああRedux下のContainerにしているという意味か。
あー。それでmapDispatchToPropsはDispatcherとReduxのつなぎ込みか。onClick eventで受け付けた場合にkickするのはsend actionで、これはdispatcherのcallbackとして登録している、と。確かにReactの本にdispatcherからのcallbackでstoreを呼び出すとある。actionが入力処理だから、えーっと、eventに対応するactionをここでは定義しているのか。
mapStateToProps関数とmapDispatchToProps関数は、それぞれStoreのstateとdispatchメソッドをpropsを通して、Container Componentで扱えるようにするものです。詳細は以下をご参照ください。
ああなんだそれだけの話か。でもなんでいちいちこんな定義しているんだろう、というのはたぶん詳細は〜で示されているpageを見るべきなんだろうな。
Rendering
最後のRenderingのところは特におかしなところもなく、普通のReactの処理。
// Rendering ReactDOM.render( <Provider store={store}> <AppContainer /> </Provider>, document.querySelector('.content') );
あーでも割と特徴があるな。
Providerでstoreを渡しているし、react-reduxでつなぎ込んだAppContainerを呼んでいるし。そういうものなのか。
あとはReduxがいかにしてReactのstate管理周りの処理を奪ってどのように回しているのかという当たりの理解が得られれば良さそう。
考察
react-redux/connect()の引数にReduxのグローバルな状態とコンポーネントのプロパティをマップする関数と、コンポーネントのプロパティをReduxのActionにマップする関数がある。
Reduxのglobal stateとcomponentのpropertyをmapしている? ああpropertyがわからないんだ。
なんだpropertyって単にdomの中に入れた定義、componentが受け取る引数のことじゃないか。this.props.xxxxで参照できる。
で、Reduxのglobal stateとcomponentのpropertyをmapしてどうするの?
ああそうか。Reduxはdata入力を受け付けてstate管理をする処理体系だから、当然mapされてないとstate変更できないわけだ。むしろmapされて当然なわけで。うん。そこはわかった。
で、propertyをmapさせるにしてもstateとmapさせるものもあればdispatcherとmapさせるものもある、と。
stateにmapさせれば、stateの変化によりviewのrenderが走る、という流れをreduxの中で解決してくれる。
dispatcherとmapさせれば、propertyの変化でdispatcherがkickされる = actionが実行されることでdispatcherが走りstoreが変更されこれをもってstateが変化しrenderがまで向かう、という処理の流れが再現される、という感じかな。
なんとなくReduxがstate管理とdata処理の部分をReactから奪ってというか、Reactで組むときに意識しなければならないところをFluxの流れで実装できるようにframeを提供してくれてるんだなというのは納得できてきた。けどmapDispatchToPropsはまだうまく納得いかないのでもう少し調べる。
ここが調べ終わったら抽象化していって、次に別の処理を追加したりしてみよう。
function mapDispatchToProps(dispatch) { return { onClick(value) { dispatch(send(value)); }, }; } const AppContainer = connect( mapStateToProps, mapDispatchToProps )(FormApp);
結局connectでcomponentのpropertyとreduxのstate、dispatchをmapしているというのはわかった。
問題はこのdispatchのmapの方。mainDispatchToPropsのonClick(value)はたぶんreduxのdispatchの定義のこと。だと思うる dispatcherはdataに応じてcallbackでstoreを呼び出し、storeでは自身に関係あるdataだった場合に編集し、change eventを発行する。
function mapDispatchToProps(dispatch)で定義されているからたぶんここの引数のdispatchがreduxのdispatcher。 dataに応じて、という部分ははまあeventのことなのかなあ。
その中で起きていることはまだわかりやすくて、reduxのdispatchに対してcallbackとなるsend(value)を引数で与えて実行している。
うーん。なんだろう。でもsendというActionをstoreにつないでおいて、storeにきたaction,dataの対に対してどのように扱うかについてはReducerで定義されているんだよな。今回はstate.valueをaction.valueに置き換えている。
とするとAction=>DispatcherはmapDispatchToPropsで定義されていて、Dispatcher=>Store=>Storeの内部処理についてはReducerで定義されている、という感じ?
Reducer = Dispatcher=>Store=>data編集、state更新 という流れの実装であることについてはこれでいい気がする。 そして今更だけどFormApp.propTypesについては単にcomponentのpropertyのtypeを定義しているだけだった。本当に。
あー。propertyとしてeventも定義されているからDispatchToPropsという名前になるのか。
どうやらproptypesはvalidatorでしかないらしい。なくてもいいけどあった方が無難。
mapDispatchToPropsで謎なのはonClick。これでどこで定義されたonClickのことだろう。それとも別の意味があるのかな。
connect()()メソッドの一つ目の引数に、Storeのstate.valueを「value」として、dispatch(send())メソッドを「onClick()」として渡し、Container Component内でpropsを通して扱えるようにします
あー。ただの名前っぽいな。これ。ActionからDispatcherが呼び出されるんだから、このDispatcherを呼び出すActionの名前ほonClick()としただけっぽい。 で、send methodがdispatcherの中で呼び出されて、storeにこれをわたす。するとreducerを用いてsendがよこしてきた情報にそってreducerの中に定義されたstateの編集処理を実行する。
ほんとか? 流れは納得できたけどonClickって本当に別の名前でもいけるのか? いじってみたけど駄目だった。やっぱりちゃんと意味はあるっぽい。この場合onClickってeventの名前じゃなくて、componentのpropertyに定義されたonClickというものを取り扱う、という意味なんだろうな、たぶん。
だから別のpropertyを定義すればそれもdispatcherへのactionの条件になる。reduxではこのpropertyの変化を監視しているみたいだから、実のところstateだろうとmethodの呼び出しだろうと関係ないのか。
他にも伝搬させたいconponentがあればここでdispatchするものを増やすことになるんだろうなあ。
たぶん大体理解できた。次はfileを分けて抽象化させてみようかな。