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を更新する。

f:id:inaz2:20160412112523p:plain

また、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")
);

上のスクリプトは、以下のような流れで動作する。

  1. テキストエリアの編集でonChange関数が呼ばれる
  2. onChange関数がUPDATE_TEXTアクションを発行する
  3. Storeに関連付けられたReducer(text関数)が新たなstateを返す
  4. 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 ...

f:id:inaz2:20160413015030p:plain

適当にテキストボックスに文字列を入力すると、入力するたびに文字数カウンタが更新されることが確認できる。

関連リンク