本記事はReduxのチュートリアルであり、Reactについての詳しい説明はここでは扱わない。
基本的にはこちらのビデオチュートリアルをベースに書いている。英語に問題がなく、ビデオが嫌いでなければこちらを参照した方が正直わかりやすいかと思う。
下準備
gitとNodeはインストール済みであるとする。まずはターミナルでスターターキットをダウンロードする。このスターターキットにはReact、Webpack、babelが含まれている(Reduxは含まれていない)。
$ git clone https://github.com/alicoding/react-webpack-babel react-counter
$ cd react-counter
$ npm install
$ npm run start
http://localhost:8888/にアクセスして正しく表示されれば成功。ターミナルでCtrl + C
を押して一度キャンセルする。
Reduxとは
Reduxは、アプリの状態を管理するためのシンプルなライブラリである。ここで、いくつか用語が出てくる。まずReduxでは、アプリの状態( state )と、状態を変更するためのアクション( action )をそれぞれ定義する。
これらは、例えばこれから作るカウントアプリでは以下のようになる。
state: アプリは現在カウンターの数値がいくつなのか、という状態。
action: 「+1をする」「-1をする」が、それぞれアクション
つまりReduxでは、stateに対してactionを実行して新しいstateにしていく。これを行う関数を reducer と呼ぶ。また、アプリは状態(state)を保持する変数を1つだけ持ち、その変数を store と呼ぶ。
すなわちアプリの状態( state )は全て store によって保持され、 reducer の関数を通して action が実行されることによってのみ変更される。そして状態が変更された際に再描画をすることで常に最新の状態がユーザーから見えるようになる。図にすると以下のようになる。
state, action, store, reducerは以後よく出てくるので、ここで理解しておきたい。
なぜReduxを使うのか?
Reactで考えるとわかりやすく、次のようなメリットがある。
- Reactのコンポーネント階層が深くなった時のバケツリレーを回避
- デバッグやテストの自動化が簡単になる
ちなみにReduxはReactに限らず他のJavascriptライブラリでも使うことができる。
Reduxのインストール
npm
を使ってインストールする。
$ npm install --save redux
カウントアプリの作成
まずはReactを使わず、Reduxのみでカウントアプリを作成する。src/index.jsx
を次のように書き換える。
import {createStore} from 'redux';
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
const store = createStore(counter);
const render = () => {
document.body.innerText = store.getState();
}
store.subscribe(render);
render();
document.addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT' });
});
npm run start
を実行してhttp://localhost:8888/にアクセスする。ページ内のどこかをクリックした時に数値が増えていけば正しく動作している。
このコードについて正しく理解しよう。まず最初の行はReduxのライブラリからcreateStore
という関数を使えるよう、インポートしている。
次にcounter
という関数を作っている。この関数が、前述したreducerになっている。引数としてstate
とaction
を受け取り、それらに応じて新しいstate
を返り値として返却する。ここでは、action.type
が'INCREMENT'のときはstateを+1し、'DECREMENT'のときはstateを-1している。
このreducerを元に、Reduxによってstore
という変数を作っている。名前の通りstoreとなる。
そして、stateが更新された際の処理としてrender
関数を定義し、subscribe
している。render
関数では単にページに最新のstate(現在のカウント)を表示している。
最後に、ページ内がクリックされた際、storeに対してactionを発行(dispatch
)している。コードの通り、actionはtype
要素を持つオブジェクトである。
createStoreの実装はどのようになっているのか?
ReduxのcreateStore
の中では何が行われているのだろうか。面白いことに、実装は以下のように非常にシンプルである(実際には例外処理など、もう少しだけ長いコードになっている)。
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener)
}
};
dispatch({});
return { getState, dispatch, subscribe };
}
コードの通り、storeはstate
と、listeners
(状態の変更を監視しているイベントリスナー達)を持つ。また、現在のstateを取得するgetState
、そしてactionを発行するdispatch
、状態の変更を監視するためのsubscribe
という3つの関数を持つ。state
やlisteners
は外部から直接変更することができないようになっている。
Reactを使って描画する
カウントアプリをReactで描画するように変更する。コードはほとんど変わらない。まずsrc/app.jsx
を以下のように変更し、数字を表示する見出しと、「+1」と「-1」の2つのボタンを置く。
import 'bootstrap/dist/css/bootstrap.min.css';
import React from 'react';
export default class App extends React.Component {
render() {
return (
<div>
<h1>{this.props.value}</h1>
<button className='btn btn-primary' onClick={this.props.onIncrement}>+</button>
<button className='btn btn-danger' onClick={this.props.onDecrement}>-</button>
</div>
)
}
}
src/index.jsx
の方ではReactDOMによってこれを描画する。全体のコードは以下のようになる。
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import App from './app.jsx';
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
const store = createStore(counter);
const render = () => {
ReactDOM.render(
<App
value={store.getState()}
onIncrement={ () =>
store.dispatch({
type: 'INCREMENT'
})
}
onDecrement={ () =>
store.dispatch({
type: 'DECREMENT'
})
}
/>,
document.getElementById('app')
)
}
store.subscribe(render);
render();
プラスボタンとマイナスボタンのあるカウントアプリが完成した!