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
のように直接指定が可能です。
実行すると、商品の一覧が表示されるはずです。
- product coke
- quanity: 1
- cost: 130
- product milk
- quanity: 3
- cost: 300
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のメリットを味わえませんが、これがある程度の規模になってくると全体で管理することのメリットが出てきます。