最近、Reduxの勉強を始めた。
Reduxとは、状態の管理を行うためのフレームワーク。
詳しいことは、ネットで検索すれば色々と出てくる。
自分もネットを使って勉強したわけであり、大いに参考になったのだが、出てくるサンプルに複雑なものが多いのが気になった。
複雑と言ってもたかが知れており、一度理解してしまえばなんてことはないのだが、もっとシンプルなほうが理解しやすい気がした。
そこで、余計な要素は出来るだけ排して、Reduxの理解に集中できるようなサンプルを作ることにした。
Reduxの最小構成
ReduxはReactと組み合わせて使うことが多いと思うが、Reactは必須というわけではなく、Redux単独でも成立する。
そこでまずは、Reduxのみを扱う。それが理解できてからReactとの連携に取り組んだほうが、理解が捗ると思う。
まず、Reduxをインストールする。
$ npm install -S redux
これで、以下のサンプルを実行できるようになる。
import { createStore } from 'redux'; const myActionCreator = (num=1) => { return { type: "INC_COUNTER", num }; }; const myReducer = (state={counter:0}, action) => { switch(action.type){ case 'INC_COUNTER': return Object.assign({}, state, { counter: state.counter + action.num }); default: return state; }; }; const myStore = createStore(myReducer); // この時点で、Reduxは機能している // 以下は動作確認 console.log(myStore.getState()); // { counter: 0 } myStore.dispatch(myActionCreator(1)); myStore.dispatch(myActionCreator(2)); console.log(myStore.getState()); // { counter: 3 }
これだけである。このファイルをbabel-node
で実行すれば、動いていることが確認できる。
babel-node
については、以下を参照。
ECMAScript(ES2015,Babel)におけるモジュールについて
ActionCreator
Reduxの中心は、Store
である。そしてStore
はstate
を持つ。
このstate
に全ての情報を格納する。
以降の話は全て、このstate
にどのように情報をセットするか、そして、その取得や変更はどのように行うのか、についてである。
Store
に対してActionCreator
とReducer
を使うことで、state
の管理を行う。
まずはActionCreator
。
先程のサンプルのこの部分が、ActionCreator
である。
const myActionCreator = (num=1) => { return { type: "INC_COUNTER", num }; };
見れば分かる通り、ただオブジェクトを返すだけの関数である。
そして、この返される関数をAction
と呼ぶ。
Action
というオブジェクトを返す関数、それがActionCreator
である。
Action
は必ず、type
という名前のプロパティを持たなければならない。その値は、文字列。
それ以外のプロパティについては、自由である。
このAction
を、次に説明するReducer
に渡すことで、state
を変更することが出来る。
Reducer
先程のサンプルのこの部分が、Reducer
。
const myReducer = (state={counter:0}, action) => { switch(action.type){ case 'INC_COUNTER': return Object.assign({}, state, { counter: state.counter + action.num }); default: return state; }; };
Reducer
は関数であり、実行する際は必ず、現在のstate
と先程説明したaction
を引数として受け取る。
そして、記述された処理を行い、新しいstate
を返す。
Reducer
の実行方法については後述する。
Reducer
内での処理は、switch()
を使ってaction.type
毎に振り分けられる。
つまり、渡されたaction
のtype
プロパティがINC_COUNTER
であれば、case 'INC_COUNTER':
ブロックの内容が実行される。
重要なのは、既存のstate
を変更するのではなく、全く別の新しいstate
を返す、という点である。
そのため、既存のstate
には手を加えず、Object.assign()
を使って新しいオブジェクトを作り、それを新しいstate
にしている。
Object.assign()
については以下を参照。
オブジェクトの値をコピーするObject.assign()
Reducer
によって返された新しいstate
をStore
が受け取り、state
の変更が完了する。
つまり、Action
をReducer
に渡すことで、state
は変更される。
そしてそれは、Store
が持つdispatch()
メソッドによって行われる。
Store
まずは、createStore()
を使ってStore
を作る。
const myStore = createStore(myReducer);
引数に、先程作ったReducer
を渡す。
これで、Reduxを構成するActionCreator
、Reducer
、Store
が全て準備できた。
後はこれを使っていくだけである。
まず、先程触れたdispatch()
メソッドについて。
サンプルにもある通り、引数にActionCreator
を渡して使う。
myStore.dispatch(myActionCreator(1));
既に説明した通り、ActionCreator
はAction
オブジェクトを返す。
つまりこの例では、以下のオブジェクトがdispatch()
に渡されたことになる。
{ type: "INC_COUNTER", num: 1 }
dispatch()
が実行されるとStore
は、createStore()
の際に登録されたReducer
を呼び出す。
その際に、現在のstate
と受け取ったAction
を、Reducer
に渡すのである。
後はReducer
の項で説明した処理が行われ、Store
のstate
が変更される。
現在のstate
を確認するには、getState()
を使う。
subscribe
Store
が持つsubscribe()
メソッドを使うと、dispatch()
される度に指定の処理を実行できるようになる。
冒頭のサンプルの動作確認の部分を以下のように書き換えると、dispatch()
が呼び出される度にログが出力される。
myStore.subscribe(()=>{ console.log('stateが変更されました。'); console.log(myStore.getState()); }); myStore.dispatch(myActionCreator(1)); myStore.dispatch(myActionCreator(2));
注意点は、dispatch()
の実行がトリガーであり、例えstate
の内容に変化が無くてもsubscribe()
は呼ばれるという点である。
Reactとの連携
Reduxの挙動を確認できたので、次は、Reactとの連携を行う。
下準備として、必要なものをインストールし、ファイルにインポートしておく。
$ npm install -S react react-dom react-redux
import { createStore } from 'redux'; // 下記を追加 import React from 'react'; import ReactDOM from 'react-dom'; import { Provider, connect } from 'react-redux';
次に、Reactのコンポーネントを作成する。
先程のサンプルの末尾に、下記の内容を書き足す。
class App extends React.Component{ render(){ return ( <div> <p>現在の数値:{this.props.counter}</p> <button onClick={ ()=>{ this.props.incCounter(1) } } >加算</button> </div> ); }; };
シンプルなコンポーネントであり、props
で渡されたcounter
を画面に表示して、ボタンを押すと同じくprops
で渡されたincCounter()
を実行する。
このprops
を、ReduxのStore
から引っ張ってこれるようにすることが、今回のゴールである。
これも先に、サンプルの全体を示しておく。
const mapStateToProps = state => { return { counter: state.counter }; }; const mapDispatchToProps = dispatch => { return { incCounter(value) { dispatch(myActionCreator(value)); } }; }; const ConnectedApp = connect( mapStateToProps, mapDispatchToProps )(App); ReactDOM.render(<Provider store={myStore}><ConnectedApp /></Provider>, document.querySelector('#mount'));
このコードを先程のサンプルの末尾に書き足すと、ReactとReduxの連携は完了する。
後はファイルをビルドしてhtmlで読み込ませれば、正しく表示され動作する。
connect
まずは、Reactのコンポーネントを、Reduxと連携できる形に変換する必要がある。
この変換にはconnect()
を使う。以下のような形になる。
const 変換後のコンポーネント = connect(Storeとpropsの接続を行う関数)(変換前のコンポーネント); // 今回のケース const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App);
変換前のコンポーネントは既にApp
として作ってあるので、後は、「Storeとpropsの接続を行う関数」を作ればいい。
今回の例だと、mapStateToProps
とmapDispatchToProps
がそれにあたる。
mapStateToPropsとmapDispatchToProps
まずはmapStateToProps
について。
const mapStateToProps = state => { return { counter: state.counter }; };
オブジェクトを返しているがこれは、
{コンポーネントに渡されるprops : Storeのstateの値}
を意味している。
そのため上記のコードにより、App
コンポーネントのthis.props.counter
には、Store
のstate.counter
への参照が格納されたことになる。
mapDispatchToProps
も、考え方は同じ。
違いは、受け取る引数がstate
ではなくdispatch
であるということ。
const mapDispatchToProps = dispatch => { return { incCounter(value) { dispatch(myActionCreator(value)); } }; };
上記のコードの結果、App
コンポーネントのthis.props.incCounter(value)
には、Store
のdispatch(myActionCreator(value))
への参照が格納されたことになる。
これにより、App
コンポーネントでボタンを押してthis.props.incCounter(1)
が実行されると、dispatch(myActionCreator(1))
を実行したことになる。
Provider
最後に、connect()
で作成したConnectedApp
をProvider
でラップしてレンダリングすれば、完了である。
ReactDOM.render(<Provider store={myStore}><ConnectedApp /></Provider>, document.querySelector('#mount'));
Provider
のstore
属性で、どのStore
と接続するかを指定する。
これで、React+Reduxの最小構成のサンプルが出来た。
後はこれを土台にして、理解を深めていけばいい。
公式のExampleとして載っているTodo Listも、基本的な処理の流れは今回のサンプルと変わらない。
connectの補足
関数の順番
connect()
を使う時は、引数として渡す関数の順番に気を付ける必要がある。
順番によって、その関数が受け取る引数が変わってしまうからである。
第一引数の関数にはstate
が渡され、第二引数の関数にはdispatch
が渡される。
そのため次のように書くと、正しく動かない。
const ConnectedApp = connect( mapDispatchToProps, // dispatchを受け取ることが期待されている関数だが、stateが渡されてしまう mapStateToProps // stateを受け取ることが期待されている関数だが、dispatchが渡されてしまう )(App);
無名関数
mapStateToProps
とmapDispatchToProps
の中身を見れば分かるが、やっていることは、props
とStore
を対にしたオブジェクトを返しているだけである。
そのため、わざわざ別の箇所で定義せずconnect()
の中で無名関数として記述しても、問題はない。
const ConnectedApp = connect( mapStateToProps, mapDispatchToProps )(App); // 上記のコードを下記のように変えても、問題なく動く const ConnectedApp = connect( (state) => ({ counter: state.counter }), (dispatch) => ({ incCounter(value){dispatch(myActionCreator(value))} }) )(App);