Redux+React超入門

前回、Reduxストアの作成やアクションのディスパッチを試しました。それではReactとReduxを接続し、Reactのコンポーネントからアクションをディスパッチにするにはどうすればいいでしょうか。

ReduxはReact同様、フレームワークではなくライブラリです。このため、ReduxはReact専用という訳ではなく、生のJavascriptやAngularといったフレームワーク及び他のライブラリと接続することもできます。

Reactと接続する場合は、react-reduxを利用します。

npm install react-redux --save-dev

以降では、前回作成したアプリケーションを用い、ReactのコンポーネントからReduxのストアを操作する簡易な例を作成します。コンポーネントの構成要素は以下です。

  • Appコンポーネント(メインコンポーネント)
  • Listコンポーネント(cartの一覧を表示するコンポーネント)
  • Formコンポーネント(新しいcartの内容を追加するコンポーネント)

また、サンプルで必要なライブラリがあるので、以下をインストールしておきましょう。

npm install uuid --save-dev

react-redux

react-redexuでコンポーネントとストアを接続するのに重要なメソッドはconnect()です。

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [Options])

mapStateToPropsは、Reduxの状態をReactのpropsに接続します。これにより、接続されたReactコンポーネントは、Reduxストア内の各情報にアクセスできます。

mapDispatchToPropsは、ReduxのアクションをReactのpropsに接続します。これにより、接続されたReactコンポーネントは、アクションをディスパッチできるようになります。

mergePropsは、mapStaetToProps、mapDispatchToProps及び親のpropsの結果を渡します。アクションクリエイターを特定のpropsに結びつけることができます。今回は使用しませんが、省略するとObject.assign({}, ownProps, stateProps, dispatchProps)が適用されます。

Provider

ReactとReduxを接続するのにもう1つ大事なのが、Providerです。ProviderはReactアプリケーションをラップします。これはReactがReduxの状態にアクセスしたり、アクションをディスパッチするためにストアとやり取りをする必要があるためです。

index.jsを以下に編集しましょう。

// src/index.js
import store from './store.js';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './components/App';

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

前述のようにProviderはReactコンポーネント(App)をラップしています。内部にはstoreがあり、これは接続するReduxストアを指定するだけです。

次に、Appコンポーネントを作成しましょう。

// src/components/App.js
import React from "react";
import List from "./List";

const App = () => (
    <div>
        <h2>Item List</h2>
        <List />
    </div>
);

export default App;

このコンポーネントはListコンポーネントをレンダリングするだけです。

Listコンポーネント

ListコンポーネントはReduxストアのcartの一覧を表示するコンポーネントです。つまり、ストアのstate.cartとListコンポーネントを結びつける必要があります。これを行うのがmapStateToPropsです。

では、Listコンポーネントを作成します。

// src/components/List.js
import React from "react";
import { connect } from "react-redux";
import uuidv1 from "uuid";

const mapStateToProps = state => {
    return {
        cart: state.shoppingCart.cart
    };
}

const ConnectedList = ({ cart }) => (
    <div>
        {cart.map(obj => {
            return (
                <ul key={uuidv1()}>
                    <li>product: {obj.product}</li>
                    <li>quanity: {obj.quanity}</li>
                    <li>unitCost: {obj.cost}</li>
                </ul>
            );
        })}
    </div>
);

const List = connect(mapStateToProps)(ConnectedList);

export default List;

mapStateToPropsでコンポーネントのprops.cartとReduxストアのcartを連結させています。第1引数stateはストアの状態が格納されており、戻り値はコンポーネントにマージされるオブジェクトを返す必要があります。

例では、state.shoppingCart.cartとなっていますが、これは前回、combineReducers()で複数のレデューサーを1つにまとめたためです。

const allReducers = {
    shoppingCart: cartReducer,
    products: productsReducer
};
const rootReducer = combineReducers(allReducers);

combineReducerを利用していない場合は、state.cartのように直接指定が可能です。

実行すると、商品の一覧が表示されるはずです。

ReduxストアのcartはステートレスなListコンポーネントと接続ができました!

Fromコンポーネント

最後に新しいcartの中身を追加するFormコンポーネントを作成します。

Listはステートレスなコンポーネントとして作成しましたが、Formはステートフルなコンポーネントとして作成します。わざわざReduxを用いてアプリケーション全体の状態を管理しているのに、なぜFormコンポーネントはステートフルにする(内部に状態を持たせる)必要があるのでしょうか。

理由は、Formが管理する状態はReduxストアで管理しているグローバルな状態と異なり、他のコンポーネントから参照される必要がないからです。ショッピングカートに入っている商品の状態(コーラが何本)という状態はアプリケーション全体で管理すると多くのコンポーネントでそれを利用するメリットがあります。一方、フォームに現在入力している値をわざわざ全体で管理しても、他のコンポーネントがそれを参照する機会があるでしょうか?こういったローカルな状態は、一般的なReact同様ローカルな状態(state)で管理すればいいのです。

Formコンポーネントを作成しましょう。やや悪いコードですが、動作には問題ありません。

// src/components/Form.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { addToCart } from "../actions/cart-actions";

const mapDispatchToProps = dispatch => {
    return {
            addToCart: cart => dispatch(addToCart(...cart)),
    };
};

class ConnectedForm extends Component {
    constructor() {
        super();
        this.state = {
            product: "",
            quanity: "",
            cost: ""
        };
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChange (event) {
        this.setState({
            [event.target.className]: event.target.value
        });
    }

    handleSubmit (event) {
        event.preventDefault();
        const { product, quanity, cost } = this.state;
        this.props.addToCart([product, quanity, cost]);
        this.setState({
            product: "",
            quanity: "",
            cost: ""
        });
    }

    render () {
        const { product, quanity, cost } = this.state;
        return (
            <form onSubmit={this.handleSubmit}>
                <label htmlFor="product">product</label>
                <input
                        type="text"
                        value={product}
                        className="product"
                        onChange={this.handleChange}
                />
                <label htmlFor="quanity">quanity</label>
                <input
                    type="number"
                    value={quanity}
                    className="quanity"
                    onChange={this.handleChange}
                />
                <label htmlFor="cost">cost</label>
                <input
                    type="number"
                    value={cost}
                    className="cost"
                    onChange={this.handleChange}
                />
                <button type="submit">ADD</button>
            </form>
        );
    }
}

const Form = connect(null, mapDispatchToProps)(ConnectedForm);

export default Form;

大事なのはアクションをReactコンポーネントにマッピングしている部分と、前述したconnect()だけです。残りはReact単体で作成する場合と何ら変わりません。

const mapDispatchToProps = dispatch => {
    return {
            addToCart: cart => dispatch(addToCart(...cart))
    };
};

上記でaddToCartというアクションクリエイターをFormコンポーネントにマッピングしています。つまり、上記のようにコンポーネントでaddToCartを呼び出すと、Reduxのアクションをディスパッチすることができます。実際にトリガーになっているは、フォームの投稿時(onSubmit)のイベントです。

connect()についてですが、FormはListコンポーネントと異なり、Reduxストアの特定の状態(cart)をマッピングする必要がないため、第1引数はnullになっています。

それでは、App.jsを編集してこのFormコンポーネントをレンダリングしましょう。

// src/components/App.js
import React from "react";
import List from "./List";
import Form from "./Form";

const App = () => (
    <div>
        <div>
            <h2>Item List</h2>
            <List />
        </div>
        <div>
            <Form />
        </div>
    </div>
);

export default App;

実行しましょう!

コンポーネントからReduxストアにcartの内容を追加することができるようになりました!

このようにReactと接続する場合は多少手間がかかります。今回のような簡易な例では、Reduxのメリットを味わえませんが、これがある程度の規模になってくると全体で管理することのメリットが出てきます。