React + Reduxでカウンターアプリの作成 (react-redux使う編)

目的

ReactとReduxを使った実装例をググるとreact-reduxを使っているものがほとんどなので、前回作ったカウンターアプリをreact-reduxを使って再実装してみることにします。
コードはGithubに。
出来上がるものはこちら

Provider

いきなりProviderという知らないコンポーネントが登場しています。
react-reduxのドキュメントを読むと、後に出てくるconnect()を使用できるようにProviderでルートのコンポーネントをラップしてstoreを渡してあげる必要があるようです。

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import Counter from './containers/Counter';
import reducer from './reducers';

const store = createStore(reducer);
const rootEl = document.getElementById('root');

ReactDOM.render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    rootEl
);

また、ここで登場しているCounterコンポーネントは通常のコンポーネントではなくContainerコンポーネントであることに注意。

Container

react-reduxでは通常のコンポーネントとContainerコンポーネントが存在します。
Containerコンポーネントでは通常のコンポーネントにdispatchやstateをpropsとしてマッピングする処理を書きます。

containers/Counter.js

import React from 'react';
import { connect } from 'react-redux';                   
import Counter from '../components/Counter';             
import { increment, decrement } from '../actions';       

function mapStateToProps(state) {
    return {value: state.value};
}
                                                         
function mapDispatchToProps(dispatch) {
    return {
        onIncrement: () => { dispatch(increment()) },    
        onDecrement: () => { dispatch(decrement()) }     
    };
}       
            
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

mapStateToProps関数では、components/Counterにpropsとして渡したいstateを
また、mapDispatchToProps関数ではcomponents/Counterにpropsとして渡したいdispatch処理をそれぞれオブジェクトで返しています。

最後にconnect()を使ってcomponents/Counterコンポーネントのpropsで使えるようにしているという感じだと思います。
次のようにcomponents/Counterコンポーネントでpropsとして使えるようになっているのが分かります。

components/Counter.js

import React, { Component } from 'react';
                               
class Counter extends Component {

    render() {
        return (
            <p>
                Clicked: {this.props.value} times
                {' '}
                <button onClick={this.props.onIncrement}>
                    +
                </button>
                {' '}
                <button onClick={this.props.onDecrement}>
                    -
                </button>
            </p>
        );
    }
}
export default Counter;

Action

前回は作成していませんでしたが、ReduxではActionはActionCreaterで作成する必要があるようです。
見ての通りオブジェクト(Action)を返す単純な関数です。

actions/index.js

export function increment() {
    return { type: 'INCREMENT' };
}

export function decrement() {
    return { type: 'DECREMENT' };
}

今回もそうですが、typeプロパティしか持っていないActionだとActionCreaterの必要性がイマイチ分かりませんが、
「APIを叩いてデータを取得する」ような処理があった場合それはActionCreaterが行うことになります。

Reducer

Reducerについては前回とさほど変わっていませんが、stateの変更の仕方が少し違います。stateは直接変更するのではなくてObject.assignを使って新しくstateを作ることで変更します。

reducers/index.js

const initState = { value: 0 };
                               
export default (state = initState, action) => {

    switch (action.type) {
        case 'INCREMENT':      
            return Object.assign({}, state, {
                value: state.value + 1          
            });
        case 'DECREMENT':
            return Object.assign({}, state, {
                value: state.value - 1          
            });
        default:
            return state;
    }
};

まとめ

Containerコンポーネントのおかげでさらにコードの見通しが良くなりました。次回は何かしらのAPIから情報を取得してリストを作るアプリを作ってみたいと思います。

参考記事

ReduxとES6でReact.jsのチュートリアルの写経