React+Reduxの最もシンプルな構成

最近、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である。そしてStorestateを持つ。
このstateに全ての情報を格納する。
以降の話は全て、このstateにどのように情報をセットするか、そして、その取得や変更はどのように行うのか、についてである。

Storeに対してActionCreatorReducerを使うことで、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毎に振り分けられる。
つまり、渡されたactiontypeプロパティがINC_COUNTERであれば、case 'INC_COUNTER':ブロックの内容が実行される。

重要なのは、既存のstateを変更するのではなく、全く別の新しいstateを返す、という点である。
そのため、既存のstateには手を加えず、Object.assign()を使って新しいオブジェクトを作り、それを新しいstateにしている。
Object.assign()については以下を参照。
オブジェクトの値をコピーするObject.assign()

Reducerによって返された新しいstateStoreが受け取り、stateの変更が完了する。
つまり、ActionReducerに渡すことで、stateは変更される。

そしてそれは、Storeが持つdispatch()メソッドによって行われる。

Store

まずは、createStore()を使ってStoreを作る。

const myStore = createStore(myReducer);

引数に、先程作ったReducerを渡す。

これで、Reduxを構成するActionCreatorReducerStoreが全て準備できた。
後はこれを使っていくだけである。

まず、先程触れたdispatch()メソッドについて。
サンプルにもある通り、引数にActionCreatorを渡して使う。

myStore.dispatch(myActionCreator(1));

既に説明した通り、ActionCreatorActionオブジェクトを返す。
つまりこの例では、以下のオブジェクトがdispatch()に渡されたことになる。

{
    type: "INC_COUNTER",
    num: 1
}

dispatch()が実行されるとStoreは、createStore()の際に登録されたReducerを呼び出す。
その際に、現在のstateと受け取ったActionを、Reducerに渡すのである。
後はReducerの項で説明した処理が行われ、Storestateが変更される。

現在の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の接続を行う関数」を作ればいい。
今回の例だと、mapStateToPropsmapDispatchToPropsがそれにあたる。

mapStateToPropsとmapDispatchToProps

まずはmapStateToPropsについて。

const mapStateToProps = state => {
  return { counter: state.counter };
};

オブジェクトを返しているがこれは、

{コンポーネントに渡されるprops : Storeのstateの値}

を意味している。
そのため上記のコードにより、Appコンポーネントthis.props.counterには、Storestate.counterへの参照が格納されたことになる。

mapDispatchToPropsも、考え方は同じ。
違いは、受け取る引数がstateではなくdispatchであるということ。

const mapDispatchToProps = dispatch => {
  return {
    incCounter(value) {
      dispatch(myActionCreator(value));
    }
  };
};

上記のコードの結果、Appコンポーネントthis.props.incCounter(value)には、Storedispatch(myActionCreator(value))への参照が格納されたことになる。

これにより、Appコンポーネントでボタンを押してthis.props.incCounter(1)が実行されると、dispatch(myActionCreator(1))を実行したことになる。

Provider

最後に、connect()で作成したConnectedAppProviderでラップしてレンダリングすれば、完了である。

ReactDOM.render(<Provider store={myStore}><ConnectedApp /></Provider>, document.querySelector('#mount'));

Providerstore属性で、どのStoreと接続するかを指定する。

これで、React+Reduxの最小構成のサンプルが出来た。
後はこれを土台にして、理解を深めていけばいい。
公式のExampleとして載っているTodo Listも、基本的な処理の流れは今回のサンプルと変わらない。

connectの補足

関数の順番

connect()を使う時は、引数として渡す関数の順番に気を付ける必要がある。
順番によって、その関数が受け取る引数が変わってしまうからである。
第一引数の関数にはstateが渡され、第二引数の関数にはdispatchが渡される。
そのため次のように書くと、正しく動かない。

const ConnectedApp = connect(
  mapDispatchToProps,   // dispatchを受け取ることが期待されている関数だが、stateが渡されてしまう
  mapStateToProps   // stateを受け取ることが期待されている関数だが、dispatchが渡されてしまう
)(App);

無名関数

mapStateToPropsmapDispatchToPropsの中身を見れば分かるが、やっていることは、propsStoreを対にしたオブジェクトを返しているだけである。
そのため、わざわざ別の箇所で定義せずconnect()の中で無名関数として記述しても、問題はない。

const ConnectedApp = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

// 上記のコードを下記のように変えても、問題なく動く

const ConnectedApp = connect(
  (state) => ({ counter: state.counter }),
  (dispatch) => ({ incCounter(value){dispatch(myActionCreator(value))} })
)(App);

参考資料