目次
1. はじめに
React + Redux を説明する場合、役割ごとにファイルを分けるのが普通です。しかし、それだと複雑になって却って分かりにくかったりしますので、1つのJavaScript内に全て詰め込んだサンプルを用意しました。
2. デモページ
以下のURLでこのウェブページを見ることができます。
デモページ
3. 画面
「足す」ボタンを押すと、フィールドに入力した数字をどんどん足していくだけです。足した結果は右側の「合計」のところに表示されます。
「クリア」ボタンを押すと、「合計」の値が 0になります。
4. JavaScript のコード(全文)
このウェブページの JavaScriptコードはすべて 1つのファイルにまとめられています。
以下がそのコードになります。
import '../../node_modules/bootstrap-sass/assets/javascripts/bootstrap.js';
import 'babel-polyfill'
import _ from 'lodash'
import React from 'react'
import { render } from 'react-dom'
import { Provider, connect } from 'react-redux'
import { createStore } from 'redux'
import PropTypes from 'prop-types'
// index.html ファイルをコピーする
require('file-loader?name=../../dist/[name].[ext]!../index.html');
//-----------------------------------
// Action creators (Actionを返す)
//-----------------------------------
const additionAction = (value) => {
return {
type: 'ADD_VALUE',
value
}
}
const clearAdditionAction = () => {
return {
type: 'CLEAR_VALUE',
}
}
//-----------------------------------
// Reducer
//-----------------------------------
const additionReducer = (state = 0, action) => {
switch (action.type) {
case 'ADD_VALUE':
return state + action.value
case 'CLEAR_VALUE':
return 0;
default:
return state
}
}
//-----------------------------------
// Component
//-----------------------------------
const AdditionComponent = ({ total, onClickToAdd, onClickToClear }) => {
let textInput;
return (
<div>
<label>足し算</label>
<input type="text"
placeholder="10"
defaultValue="100"
ref={(input) => { textInput = input; }}
/>
<button onClick={() => onClickToAdd(textInput.value) }>足す</button>
<button onClick={onClickToClear}>クリア</button>
<span className="result">合計:{total}</span>
</div>
);
};
AdditionComponent.propTypes = {
total: PropTypes.number.isRequired,
onClickToAdd: PropTypes.func.isRequired,
onClickToClear: PropTypes.func.isRequired
};
//-----------------------------------
// Container
//-----------------------------------
const AdditionContainer = (() => {
const mapStateToProps = (state/*, ownProps*/) => {
const props = {
total: state
};
return props;
}
const mapDispatchToProps = (dispatch) => {
return {
onClickToAdd: (value) => {
const val = parseInt(value);
if (_.isNaN(val)) {
//
} else {
dispatch(additionAction(val));
}
},
onClickToClear: () => {
dispatch(clearAdditionAction());
}
}
}
return connect(
mapStateToProps,
mapDispatchToProps
)(AdditionComponent);
})();
//-----------------------------------
// Store
//-----------------------------------
const store = createStore(additionReducer)
//-----------------------------------
// 画面に表示する
//-----------------------------------
render(
<Provider store={store}>
<AdditionContainer />
</Provider>,
document.getElementById('root')
)
5. 解説
React と Redux にとって重要な部分を説明します。
1. Action (Action Creator)
Action は「何が起きるか?」を表します。
今回、「値を足す」と「値をクリアする」という2つの処理がありますので、Action(とAction Creator)も2つ用意しました。
adddionAction
- 「値を足す」を表す action creator です。
ADD_VALUE
という文字列で表すことにします。- 足す値を引数として受け取って、return で返すオブジェクト(Action)に追加しています。
clearAdditionAction
- 「値をクリアする」を表す action creator です。
CLEAR_VALUE
という文字列で表すことにします。- こちらはそれ以外で必要な値はありません。
const additionAction = (value) => {
return {
type: 'ADD_VALUE',
value
}
}
const clearAdditionAction = () => {
return {
type: 'CLEAR_VALUE',
}
}
※ ここでは ADD_VALUE
と CLEAR_VALUE
を文字列にしましたが、実際のアプリケーションでは定数にした方がよいでしょう(ESLintなどのツールがタイプミスを発見してくれます)。
2. Reducer
この小さなアプリケーションで State として管理する値は、「足した結果の値」だけとしました。実際は、エラーがあった場合とそうでない場合を区別するための値などもあった方がよいですが、焦点がぼやけそうなので省略しました。
以下の Reducer は、この「足した結果の値」を更新するために用意しました。この値の更新に関する処理は、この中に書きます。
今回扱う 2つの処理もこの値を更新する処理ですので、action.type
で区別してここで処理しています。Reducer は「現在のstate」と「action」を引数として受け取り、「新しいstate」を返します。
「値を足す」ADD_VALUE
の場合は、現在の値に、actionとして渡ってきた値を足した数を返しています。
「値をクリアする」CLEAR_VALUE
の場合は、単に 0 を返しています。
const additionReducer = (state = 0, action) => {
switch (action.type) {
case 'ADD_VALUE':
return state + action.value
case 'CLEAR_VALUE':
return 0;
default:
return state
}
}
3. Component
画面となる部分です。
属性(props)として、以下の3つを受け取っています。
total
: 足した結果の値onClickToAdd
: 「足す」ボタンをクリックした時のイベントハンドラonClickToClear
: 「クリア」ボタンをクリックした時のイベントハンドラ
この2つのイベントハンドラは、Container 側で実装するのですが、これが Redux のやり方です。Container 内では、State を更新するための dispatch
関数を使うことができます。
「値を足す」方のイベントハンドラでは、画面上に入力された値を取得して使うことになるので、ここでは onClickToAdd
にその値を渡すような形にしています。これを実現するため、onClick=
のところで「入力された値を onClickToAdd
に渡して実行する」という処理を無名関数を作って指定しています。少しトリッキーなやり方に思えますが、他によいやり方もないように思います。
total
はそのまま画面に出力しているだけです。
最後に、propTypes
で各値を定義しておきましょう。
const AdditionComponent = ({ total, onClickToAdd, onClickToClear }) => {
let textInput;
return (
<div>
<label>足し算</label>
<input type="text"
placeholder="10"
defaultValue="100"
ref={(input) => { textInput = input; }}
/>
<button onClick={() => onClickToAdd(textInput.value) }>足す</button>
<button onClick={onClickToClear}>クリア</button>
<span className="result">合計:{total}</span>
</div>
);
};
AdditionComponent.propTypes = {
total: PropTypes.number.isRequired,
onClickToAdd: PropTypes.func.isRequired,
onClickToClear: PropTypes.func.isRequired
};
4. Container
先ほど作った Component を包み込み、connect()
関数で、Redux の世界につなげます。
必要に応じて、以下の2種類の関数を作成します。
mapDispatchToProps
- 更新された state が引数として渡ってきますので、それを component の props に渡す処理を記述します。
- 今回はそのまま、props.total として渡しています。
mapDispatchToProps
- component 内で使用するイベントハンドラは、ここで定義します。
- 画面上に入力された値を使った処理を書きたい場合は、引数としてその値を受け取る形にしておくのが通常のやり方です。component 側では入力した値をここで定義したイベントハンドラに渡して実行します。
- ここで定義したイベントハンドラは、component に props として渡されます。
const AdditionContainer = (() => {
const mapStateToProps = (state/*, ownProps*/) => {
const props = {
total: state
};
return props;
}
const mapDispatchToProps = (dispatch) => {
return {
onClickToAdd: (value) => {
const val = parseInt(value);
if (_.isNaN(val)) {
//
} else {
dispatch(additionAction(val));
}
},
onClickToClear: () => {
dispatch(clearAdditionAction());
}
}
}
return connect(
mapStateToProps,
mapDispatchToProps
)(AdditionComponent);
})();
5. Store
Store は state を管理しているところです。state を更新する Reducer もまとめて保持しますので、createStore()
関数に Reducer を渡して Storeを作成します。
const store = createStore(additionReducer)
6. 画面に表示する
Redux が用意している Provider
という component で、先ほど作成した container を包んで画面にレンダリングします。その際、Provier
に store を渡します。これにより、ここまで定義してきたすべてのオブジェクトが1つにまとめられてアプリケーションとして機能します。
render(
<Provider store={store}>
<AdditionContainer />
</Provider>,
document.getElementById('root')
)
6. 動作
「足す」ときの処理は以下のように動きます。
- 画面上の「足す」ボタンを押すと、
onClickToAdd
イベントハンドラが実行されます。 onClickToAdd
イベントハンドラは、- 画面上に入力された値を受け取り、
- それを
additionAction
という action creator に渡して action を生成します。 - それを、
dispatch()
関数に渡して実行します。
dispach()
関数は内部で各Reducerを呼び出して実行します。- 今回作成した
additionReducer
という Reducer では、ADD_VALUE
を持った action を受け取りますので、『「現在の足した値」と「画面上の値」を足した値』を返します。 - state は、新しい値で更新されます。
mapStateToProps
に更新された state (足した結果の値)が渡されます。mapStateToProps
は渡された値をそのまま、component に渡します。- component は新しい値を受け取り、画面を更新します。
開発する側にとってのイメージとしては、
- どんな処理があるかを Action (と Action Creator) で表す。
- Reducer で state を更新する処理を実装しておく。
- component と container で、以下を実装しておく。
- 画面を見た目を更新する処理 (React)
- イベントがあった時に Redux に通知する処理 (イベントハンドラでdispatchする。その際、処理の種類に合ったActionを渡す。)
をやっておけば、あとはユーザーの操作によってイベントが発生すると、
- component
- –> reducer
- –> stateの更新
- –> component に戻って画面更新
という流れで処理を行ってくれるという感じです。
「各state を更新する処理」と「各stateを元に画面を更新する処理」が分離することによって、アプリケーションが複雑になっても、実装はそれ程複雑にならない。というメリットがあります。
7. まとめ
すごく単純な処理を実装したいだけであれば、Redux + React を使うのは却って面倒なだけでしょう。どのくらいの規模から、メリットが大きくなるのかを見極めるために、今回のような最小限のサンプルが役に立つのではないかと思います。