ReactにReduxを組み合わせて使ってみる
「Webpack+BabelでReactを使ってみる」では、Reactを使って簡単なアプリケーションを書いた。 ReactはMVVM(Model-View-ViewModel)におけるV層を扱うモジュールである一方、VM層に相当するものとしてReduxがある。 ここでは、ReactにReduxを組み合わせて同様のアプリケーションを書いてみる。
環境
Ubuntu 14.04.4 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.4 LTS Release: 14.04 Codename: trusty $ nodejs --version v4.4.2 $ npm --version 2.15.0
Reduxとは
JavaScriptアプリケーションのMVVMにおけるVM層のデザインパターンのようなもの。 GoFデザインパターンにおけるCommandパターン、Observerパターンのイメージに、次の3原則を加えたものだと考えるとイメージしやすい。
- Single source of truth
- The state of your whole application is stored in an object tree within a single store.
- State is read-only
- The only way to mutate the state is to emit an action, an object describing what happened.
- Changes are made with pure functions
- To specify how the state tree is transformed by actions, you write pure reducers.
すなわち「単一のstoreがすべてのstateを管理」「stateは書き換えず置き換え」「変化は副作用を持たない関数(純粋関数)で表現」の三つである。
Reduxの概要
ReduxはAction、Reducer、Store、Viewの四つの概念からなる。 具体的には、ユーザの操作によりActionが発行され、Storeに関連付けられたReducerがActionに応じて新たなstateを返す。 そして、Storeに登録された描画(render)関数が新たなstateに応じてViewを更新する。
また、Reduxは先発するフレームワークFluxの影響を受けている。 Fluxとの違いは、Dispatcherの概念を純粋関数で表されたReducerの合成で置き換えていること、stateの破壊的変更を行わないことである(参考)。 これにより、Reduxはよりすっきりした形でstateを扱えるようになっている。
ディレクトリの作成
$ mkdir helloredux $ cd helloredux/ $ npm init -y $ npm install --save-dev webpack babel-loader babel-core babel-preset-react babel-preset-es2015 $ npm install --save react react-dom $ npm install --save redux react-redux
package.jsonを編集し、npmからWebpackを実行できるようにする。
{ "name": "helloredux", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rm -rf scripts/*.js && webpack", "watch": "rm -rf scripts/*.js && webpack -w" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "babel-core": "^6.7.6", "babel-loader": "^6.2.4", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "webpack": "^1.12.15" }, "dependencies": { "react": "^15.0.1", "react-dom": "^15.0.1", "react-redux": "^4.4.5", "redux": "^3.4.0" } }
次に、webpack.config.jsを作成し、Babelで./src/app.js
を./scripts/bundle.js
にコンパイルするよう指定する。
module.exports = { entry: "./src/app.js", output: { path: __dirname + "/scripts", filename: "bundle.js" }, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", query: { presets: ["react", "es2015"] } } ] } };
最後に、ソースファイルとコンパイル後のスクリプトを置くディレクトリを作成する。
$ mkdir src scripts
文字数カウンタを書いてみる
まず、コンパイルされたJavaScriptを読み込んで実行するHTMLファイルを作成する。
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Hello, Redux!</title> </head> <body> <div id="content"></div> <script type="text/javascript" src="scripts/bundle.js"></script> </body> </html>
次に、ReactおよびReduxを使いテキストボックスへの入力に合わせて文字数を表示するスクリプトを書く。
// src/app.js import React from "react"; import ReactDOM from "react-dom"; import { createStore } from 'redux'; import { Provider, connect } from 'react-redux'; // Reducer has the interface `(state, action) => state` const text = (state = { text: "" }, action) => { switch (action.type) { case 'UPDATE_TEXT': return { text: action.text }; default: return state; } }; const mapStateToProps = (state) => { return { text: state.text }; }; const mapDispatchToProps = (dispatch) => { return { onChange: (e) => dispatch({ type: 'UPDATE_TEXT', text: e.target.value }) }; }; let WordCountBox = ({ text, onChange }) => { return ( <div className="wordCountBox"> <h1>Hello, Redux!</h1> <textarea rows="8" cols="80" placeholder="Type something..." autoFocus="true" onChange={onChange}> {text} </textarea> <p>Count: {text.length}</p> </div> ) }; WordCountBox = connect(mapStateToProps, mapDispatchToProps)(WordCountBox); ReactDOM.render( <Provider store={createStore(text)}> <WordCountBox /> </Provider>, document.getElementById("content") );
上のスクリプトは、以下のような流れで動作する。
- テキストエリアの編集でonChange関数が呼ばれる
- onChange関数が
UPDATE_TEXT
アクションを発行する - Storeに関連付けられたReducer(
text
関数)が新たなstateを返す - stateの更新に合わせてビューが更新される
Reducerは、関心外のactionに対しては元のstateをそのまま返す。
これにより、さまざまなカテゴリに関するReducerをまとめて一つの関数群として扱えるようになる。
ここでは使用していないが、ReduxではcombineReducers
関数を用いることで複数のReducerを一つにまとめることができる。
また、react-reduxモジュールはProviderクラスとconnect関数を提供する。 これらにより、Providerクラスに結び付けられたStoreを透過的に扱うことができる。
コンパイルして表示してみる
npm経由でWebpackを実行し、上のスクリプトをJavaScriptコードにコンパイルしてみる。
$ npm run build > helloredux@1.0.0 build /home/user/tmp/helloredux > rm -rf scripts/*.js && webpack Hash: 9a0098997f2d807e400b Version: webpack 1.12.15 Time: 4798ms Asset Size Chunks Chunk Names bundle.js 738 kB 0 [emitted] main + 189 hidden modules
Python等を用いてWebサーバを起動した後、ブラウザでindex.htmlにアクセスした際のスクリーンショットを示すと次のようになる。
$ python -m SimpleHTTPServer Serving HTTP on 0.0.0.0 port 8000 ...
適当にテキストボックスに文字列を入力すると、入力するたびに文字数カウンタが更新されることが確認できる。