RailsでReduxコンテナをサーバサイドレンダリング (use Hypernova by airbnb)


## はじめに
[前回の記事 RailsでReactをサーバサイドレンダリング（use Hypernova by airbnb） - Qiita](http://qiita.com/noriaki/items/e2dac69b9dd88334dd43)では、ReactコンポーネントをRailsアプリで簡単にサーバサイドレンダリングに対応する方法を書きました。

最近ではReact単体ではなく、コンポーネント群の状態(State)を一元管理してデータの流れを一方向に整える開発技法`Flux`を取り入れた`Redux`(`react-redux`)を用いたアプリケーションも増えてきました。

前回ご紹介した`Hypernova`では、Reactコンポーネントのサーバサイドレンダリングには対応していたのですが、Reduxコンテナ[^1]はうまくレンダリング出来ませんでした。

そこで、この記事ではReduxコンテナのサーバサイドレンダリングに対応したHypernovaのReact/Reduxバインディングである[noriaki/hypernova-react-redux](https://github.com/noriaki/hypernova-react-redux)を紹介すると共に、シンプルなカウンターアプリをサーバサイドレンダリングで表示するまでのチュートリアルを解説します。

[前回記事のチュートリアル](http://qiita.com/noriaki/items/e2dac69b9dd88334dd43)を完了している前提で、変更点を追いかけるかたちで書いていきますので、まだの方は先に試してみてください。

## Hypernova/ReactRedux
airbnb製のHypernova Reactバインディング([airbnb/hypernova-react](https://github.com/airbnb/hypernova-react))を改良して、Reduxコンテナのサーバサイドレンダリングが可能になるHypernova用バインディングとして作成しました。

[noriaki/hypernova-react-redux: React/Redux (react-redux) bindings for Hypernova.](https://github.com/noriaki/hypernova-react-redux)

`Hypernova/server`や`Hypernova/ruby`のコードを変更することなく、`Hypernova/react`を置き換えることが出来るように設計しています。

## Hypernova/ReactReduxを使ったサーバサイドレンダリングのチュートリアル
Reduxを利用したカウント{アップ|ダウン}ができるカウンターアプリを実装します。
また、ES2015形式やReact JSX形式の記載ができるよう`babel`によるトランスパイラも使いますが、トランスパイルは`browserify-rails`が実行してくれますのでご安心ください。

### 環境・バージョン
前回記事と同様ですが再掲しておきます。

```sh:environments
# mac
MacBook Air (13-inch, Mid 2012) - OS X El Capitan (10.11.5)

# ruby/rails
% ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]
% gem -v
2.5.1
% rails -v
Rails 4.2.6

# node/npm
% node -v
v5.5.0
% npm -v
3.7.1
```

### 1. Railsアプリの作成 〜 3. browserify-railsの導入
[前回記事のチュートリアル](http://qiita.com/noriaki/items/e2dac69b9dd88334dd43)と同様です。

### 4. JavaScriptの環境設定 (package.json)
ES2015やReact JSXの変換のためにいくつかパッケージを追加しています。

#### 前回記事のチュートリアルの続きの人
以下のコマンドを実行してパッケージを追加します。

```sh
% npm i -D babelify babel-preset-es2015 babel-preset-react babel-cli
% npm i -S hypernova-react-redux react-redux redux
```

```diff:package.jsonの差分
   "dependencies": {
     "hypernova": "^1.0.0",
-    "hypernova-react": "^1.0.0",
+    "hypernova-react-redux": "^1.0.0",
     "react": "^15.1.0",
-    "react-dom": "^15.1.0"
+    "react-dom": "^15.1.0",
+    "react-redux": "^4.4.5",
+    "redux": "^3.5.2"
   },
   "devDependencies": {
+    "babel-cli": "^6.10.1",
+    "babel-preset-es2015": "^6.9.0",
+    "babel-preset-react": "^6.11.1",
+    "babelify": "^7.3.0",
     "browserify": "^13.0.1",
     "browserify-incremental": "^3.1.1"
   }
```

#### 今回記事のチュートリアルからの人
完成した`package.json`を載せておきますので、`#{Rails.root}`にファイルを作り`npm install`を実行します。

```json:完成したpackage.json
{
  "name": "hypernova-getting-started-with-rails",
  "version": "1.0.0",
  "description": "A sample Rails application that uses Hypernova to server render.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\""
  },
  "author": "Noriaki Uchiyama",
  "license": "MIT",
  "dependencies": {
    "hypernova": "^1.0.0",
    "hypernova-react-redux": "^1.0.0",
    "react": "^15.1.0",
    "react-dom": "^15.1.0",
    "react-redux": "^4.4.5",
    "redux": "^3.5.2"
  },
  "devDependencies": {
    "babel-cli": "^6.10.1",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.11.1",
    "babelify": "^7.3.0",
    "browserify": "^13.0.1",
    "browserify-incremental": "^3.1.1"
  }
}
```

### 5. Hypernova/serverの設定 〜 6. Hypernova/rubyの設定
[前回記事のチュートリアル](http://qiita.com/noriaki/items/e2dac69b9dd88334dd43)と同様です。

### 7. Hypernova/react-reduxを使用したReduxコンテナ
ここが前回と大きく違います。前回、`props`から文字列を表示するコンポーネントを実装していた`MyComponent.js`を、以下のようにごっそり置き換えます。

```js:app/assets/javascripts/MyComponent.js
import React, { PropTypes } from 'react';
import { renderReactRedux } from 'hypernova-react-redux';

import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';

// Component
function Counter(props) {
  const { count, onIncrement, onDecrement } = props;
  return (
    <div>
      <p>カウント: {count}回</p>
      <button onClick={onIncrement}>++</button>
      <button onClick={onDecrement}>--</button>
    </div>
  );
}
Counter.propTypes = {
  count:       PropTypes.number.isRequired,
  onIncrement: PropTypes.func.isRequired,
  onDecrement: PropTypes.func.isRequired
};

// Actions
const INCREMENT_COUNTER = {
  type: 'INCREMENT_COUNTER'
};
const DECREMENT_COUNTER = {
  type: 'DECREMENT_COUNTER'
};

// Reducer
function counterReducer(state = { count: 0 }, action) {
  switch(action.type) {
  case 'INCREMENT_COUNTER':
    return { count: state.count + 1 };
  case 'DECREMENT_COUNTER':
    return { count: state.count - 1 };
  default:
    return state;
  }
}

// configureStore
function configureStore(preloadState = {}) {
  const initialState = counterReducer(undefined, {});
  return createStore(
    counterReducer,
    Object.assign({}, initialState, preloadState)
  );
}

// connect (react-redux)
function mapStateToProps(state) {
  return { count: state.count };
}

function mapDispatchToProps(dispatch) {
  return {
    onIncrement() { return dispatch(INCREMENT_COUNTER); },
    onDecrement() { return dispatch(DECREMENT_COUNTER); }
  };
}

let ConnectedMyComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter);


module.exports = renderReactRedux(
  'MyComponent.js',
  ConnectedMyComponent,
  configureStore
);
```

前回と同様、重要なのは`renderReactRedux`関数で、第一引数にコンポーネント名、第二引数にコンポーネント実体を受け取る点は`renderReact`関数と同じですが、第三引数に`configureStore`関数[^2]を受け取っています。

なお、通常このファイルに記載している内容は、`Component`や`Action`、`Reducer`などは別ファイルに分けるのですが、今回はすべて一つのファイルに記載しました。

別ファイルに分ける場合、`MyComponent.js`は以下のようになります。（各コンポーネントの`import`元は適切に書き換えてください）

```js:app/assets/javascripts/MyComponent.js
import renderReactRedux from 'hypernova-react-redux';
import ConnectedMyComponent from './PATH/TO/Containers/ConnectedComponent';
import configureStore from './PATH/TO/Store/configureStore';

module.exports = renderReactRedux(
  'MyComponent.js',
  ConnectedMyComponent,
  configureStore
);
```

### 8. RailsアプリからHypernova呼び出し
前回記事のチュートリアルからいくつか変更を加えています。
`app/controllers/welcome_controller.rb`の`hypernova_render_support`メソッドや、`app/views/layouts/application.html.erb`は前回の記事を参考にしてください。

#### 8.1. Viewからコンポーネントを呼び出す
`app/views/welcome/index.html.erb`を以下のいずれかに書き換えます。

##### State情報をRailsから渡さない（Reduxカウンターアプリ内の初期Stateを利用する）場合

```html+erb:app/views/welcome/index.html.erb
<%= render_react_component('MyComponent.js') %>
```

##### RailsからState情報を渡す場合
この場合カウンターの初期値が`10`になります。

```html+erb:app/views/welcome/index.html.erb
<%= render_react_component('MyComponent.js', count: 10) %>
```

#### 8.2. Babelによるトランスパイル設定
(7)のReduxコンテナをES2016やReact JSXの記法で実装したので、`browserify-rails`がこの記法を変換できるように以下を追記します。

```rb:config/application.rb
# browserify-rails with babelify (es2015, react)
config.browserify_rails.commandline_options =
  "-t [ babelify --presets [ es2015 react ] ]"
```

### 9. 動作を確認してみる
Railsアプリを起動します。

```sh
% bin/rails server
```

起動したら、ブラウザで http://localhost:3000/welcome/index へアクセスし、`カウント: 0回`などと表示され、`++`や`--`のボタンクリックでカウント表示が増減することが確認できるでしょう。

ページのソースを確認してみると、該当部分は以下のようになっていると思います。

```js+erb
<div data-hypernova-key="MyComponentjs"></div>
<script type="application/json" data-hypernova-key="MyComponentjs"><!--{}--></script>
```

前回も出てきましたが、これは`Hypernova/server`を起動していないので **サーバサイドレンダリングを利用していない** アプリとして動作しています。

#### サーバサイドレンダリング機能を有効にする
`Hypernova/server`サービスを起動すると、他のコードは変更しなくてもサーバサイドレンダリングが有効になります。

今回は`babel`によるトランスパイルが必要になっているので、サービス起動のためのnpmスクリプトを設定し利用します。具体的には`package.json`に以下を追加します（diffによる差分）。

```diff:package.jsonの差分
   "main": "index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\""
+    "test": "echo \"Error: no test specified\"",
+    "hypernova": "babel-node hypernova.js --presets es2015,react"
   },
   "author": "Noriaki Uchiyama",
   "license": "MIT",
```

これで、`npm run hypernova`というコマンドが使用出来るようになります。

```sh
% npm run hypernova

> hypernova-getting-started-with-rails@1.0.0 hypernova /path/to/rails/app/hypernova-getting-started-with-rails
> babel-node hypernova.js --presets es2015,react

2016-07-03T09:17:15.470Z - info: Connected
{ port: 3030 }
```

正常にサービスが起動すると上記のように`Connected`と表示されます。

この状態で再度Railsアプリ http://localhost:3000/welcome/index へアクセスすると、表示内容やクリック時の動作はサーバサイドレンダリングを利用しないときと同じですが、ページのソースを確認すると以下のように直接HTMLにコンポーネントが出力され **サーバサイドレンダリングを利用している** ことが分かります。

```js+erb
<div data-hypernova-key="MyComponentjs"><div data-reactroot="" data-reactid="1" data-react-checksum="789933384"><p data-reactid="2"><!-- react-text: 3 -->カウント: <!-- /react-text --><!-- react-text: 4 -->0<!-- /react-text --><!-- react-text: 5 -->回<!-- /react-text --></p><button data-reactid="6">++</button><button data-reactid="7">--</button></div></div>
<script type="application/json" data-hypernova-key="MyComponentjs"><!--{}--></script>
```

という感じで、簡単にサーバサイドレンダリングを利用できるようになりました。

## さいごに
本記事では、Reduxコンテナのサーバサイドレンダリングに対応したHypernovaのReact/Reduxバインディングである[noriaki/hypernova-react-redux](https://github.com/noriaki/hypernova-react-redux)を使った、シンプルなカウンターアプリをサーバサイドレンダリングで表示するまでのチュートリアルを解説しました。

`Hypernova`を使えば、`Rails`でデータベース接続やAPIを実現し、フロントエンドは`React`/`Redux`によるシングルページアプリケーションを構築することも出来るでしょう。

ぜひ試してみてください。
もしお役に立ちそうなら「ストック」してもらえると嬉しいです。

なお、[noriaki/hypernova-react-redux](https://github.com/noriaki/hypernova-react-redux)では、[バグ発見や要望](https://github.com/noriaki/hypernova-react-redux/issues)、[プルリクエスト](https://github.com/noriaki/hypernova-react-redux#contribution)などをお待ちしています。

## 参考

- [noriaki/hypernova-react-redux](https://github.com/noriaki/hypernova-react-redux)
- [Redux Documents](http://redux.js.org/index.html)

[^1]: Reduxでは、Store（すべてのStateを管理する単一のオブジェクト）を参照できる接続されたコンポーネント(Connected Component)をコンテナ(Container)と呼びます
[^2]: `redux`パッケージの`createStore`関数を内部で実行し、その結果を返す関数です http://redux.js.org/docs/basics/Store.html