ReduxでFluxなアプリを実装する(2)
カウンターアプリのRedux部分を実装する
それでは、カウンターアプリの状態管理をReduxで実装していきましょう。Redux特有の部分として、次のファイルを作成します。
- src/actions/counter.js
- src/reducers/index.js
- src/reducers/counter.js
それぞれの実装内容について説明していきます。まずはActionを作ります。src/actions/counter.jsを作成しましょう。
// amountの数だけカウントを増やすためのActionを作成する
export function increment(amount) {
return {
type: 'INCREMENT', // (1)
payload: { // (2)
amount
}
};
}
// amountの数だけカウントを減らすためのActionを作成する
export function decrement(amount) {
return {
type: 'DECREMENT',
payload: {
amount
}
};
}
これらはActionを表すオブジェクトを返す関数で、Action Creatorと呼ばれます。Actionはただのオブジェクトであればよく、(1)の通りtypeプロパティを持っていることだけが必須の条件です。type以外は何を書いてもよいのですが、慣例として(2)の通りpayloadプロパティの中にパラメータを置いています。この関数によって作られたActionオブジェクトをdispatch関数に渡すことで、データフローは動き出すのです。
[コラム]Flux Standard Action
Actionのデータ構造はかなり緩いものですが、あまりに自由すぎるルールは混乱を招きます。普通にデータを渡したり、発生したエラーについて通知したりするといった、ありふれたユースケースくらいは、標準化されていてほしいものです。そんなモチベーションから有志によって策定された規格が「Flux Standard Action」です。
Flux Standard ActionにのっとってActionを作る場合は、通常の規定とは別に、次のルールを守りましょう。
-
データを渡す場合には
payloadプロパティにオブジェクトを定義して渡すこと -
エラーを表すActionである場合には
errorプロパティをtrueにし、payloadにエラーオブジェクトを渡すこと -
もし付加情報を
payloadとは別に通知したい場合にはmetaプロパティに渡すこと
これにのっとると、通常のデータを渡すActionはリスト4のように定義することになります。
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
また、エラーが発生した場合にはリスト5の通り定義することになります。多くの場合、これはtry-catch構文のcatch句の中でdispatchされ、catch句からもらったエラーオブジェクトをpayloadに渡すことになります。
{
type: 'ERROR_IN_ADD_TODO',
payload: new Error("has error!"),
error: true
}
今回は詳しく紹介できませんが、Flux Standard ActionにのっとったActionやReducerを簡単に定義するためのライブラリとしてredux-actionsといったライブラリもRedux向けに提供されています。Reduxに慣れてきた頃に、導入を検討してみるといいでしょう。
次に、このActionを受け取るReducerの実装を見ていきましょう。まずはsrc/reducers/counter.jsを作成します。
const defaultState = { // (5)
count: 0
};
const counter = (/* (1) */state = defaultState, /* (2) */action) => {
switch(action.type) {
case 'INCREMENT':
return { // (3)
count: state.count + action.payload.amount
};
case 'DECREMENT':
return {
count: state.count - action.payload.amount
};
default:
return state; // (4)
}
};
export default counter;
Reducerはひとつのオブジェクト(state)をactionに応じて変換し、新しいオブジェクトに作り直して返却する関数です。(1)が変更前の現在の状態で、(2)が前述のActionをdispatchした結果として送り込まれてきたものです。Actionのtypeに応じて処理を振り分け、最終的には(3)の通り変更後の状態を新しいオブジェクトとして返します。ReducerにはあらゆるActionが渡されるので、関係ないActionが届いたときには(4)の通りstateをそのまま返し、何も起こらないようにしておくことも重要です。また、起動した時点では「現在の状態」というものが存在しないので、(5)のデフォルト引数で初期状態を定義しておくとよいでしょう。
こうして作られた、counterという名前のReducerが管理するstateは、次のデータ構造になります。
{
count: 0
}
さて、Reducerひとつで管理できる状態オブジェクトはひとつだけですが、複数のReducerを使ってさまざまな状態を管理することができます。そのための記述をsrc/reducers/index.jsに行います。
import { combineReducers } from 'redux'
import counter from './counter';
const rootReducer = combineReducers({
counter: counter // (1)
});
export default rootReducer;
(1)の通り、combineReducersに渡すオブジェクトにReducerを登録していくことで、複数のReducerを取りまとめて管理することができます。こうしてできたrootReducerのデータ構造は次の通りになります。
{
counter: {
count: 0
},
// リスト8の(1)でReducerを増やすと、ここにデータが追加されていく
}
リスト6のような関数もReducerですし、それらを束ねてリスト8のように加工したものもReducerです。少し混乱しそうになりますが、そういうものだと理解してください。
ここまでで、Redux特有のコードが作成できました。
カウンターアプリのReact部分を実装する
それでは、ReduxのコードをReactにつなぎこんでいきましょう。まずはエントリーポイントであるindex.jsの変更です。
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import rootReducer from './reducers';
import App from './App';
// reducerを元にstoreを生成する
const store = createStore(rootReducer); // (1)
ReactDOM.render(
<Provider store={store}> {/* (2) */}
<App />
</Provider>,
document.getElementById('root')
);
Reducerは、(1)でstoreに取り込まれて、状態管理に使われます。(2)では、ReactコンポーネントであるProviderにstoreを渡すことで、App以下のすべてのReactコンポーネントがstore内のデータを受け取れるようになります。
これで、AppコンポーネントにReduxのデータが渡るお膳立てができました。あとはsrc/App.jsを変更するのみです。App.jsは次のようになります。
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { increment, decrement } from './actions/counter';
class App extends Component {
/** +1ボタンを押した際にactionを発行する処理 */
_handlePlusOneClick(e) {
this.props.dispatch(increment(1)); // (4)
}
/** -1ボタンを押した際にactionを発行する処理 */
_handleMinusOneClick(e) {
this.props.dispatch(decrement(1));
}
render() {
const count = this.props.count; // (3)
return (
<div>
<button onClick={e => this._handleMinusOneClick(e)}>-1</button>
<span>{count}</span>
<button onClick={e => this._handlePlusOneClick(e)}>+1</button>
</div>
);
}
}
/** store内のデータをpropsに変換する */
function mapStateToProps(state) { // (2)
return {
count: state.counter.count
};
}
export default connect(mapStateToProps)(App); // (1)
Appコンポーネントへの変更で最も重要なのは、(1)のconnect関数です。ReduxとReactコンポーネントを「つなげる」役割を果たしてくれます。大別すると、次のふたつの機能があります。
- Reducerによって分割管理されたstate(store内の状態データ)をReactのpropsに加える
- dispatchをReactのpropsに加える
まず前者ですが、(2)のmapStateToPropsで定義した関数によって、Reducerごとに保存された状態をコンポーネントのpropsに変換することができます。これによって、(3)の通りコンポーネントの中でReducer由来の値を使えるようになります。(2)の引数に渡されるstateは、リスト9で紹介した、rootReducerが管理しているデータ構造です。
そして後者として、コンポーネント内でpropsからdispatch関数がもらえるようになります。これにより、(4)の通りActionを発行できるようになります。increment(1)の戻り値はリスト6で実装した通り、typeを持つオブジェクト=Actionです。これをdispatchに渡すことで、Actionが発行されます。
これでカウンターアプリが動くようになりました。
まとめ
今回はFluxとReduxの基礎的な部分を紹介しました。今回作ったActionのパラメータを変えれば「+5」ボタンのといったものも簡単に作れますし、新たに「リセット」ボタンを作ってみるのも面白いかと思います。配布しているサンプルコードにはそういった機能も追加しているので、興味がある方は見てください。
次回は、Reduxを実践的な状況でも扱えるようにするために、非同期処理を題材にして深い使い方を学んでいきます。
