はじめに
前回の記事 RailsでReactをサーバサイドレンダリング(use Hypernova by airbnb) - Qiitaでは、ReactコンポーネントをRailsアプリで簡単にサーバサイドレンダリングに対応する方法を書きました。
最近ではReact単体ではなく、コンポーネント群の状態(State)を一元管理してデータの流れを一方向に整える開発技法Flux
を取り入れたRedux
(react-redux
)を用いたアプリケーションも増えてきました。
前回ご紹介したHypernova
では、Reactコンポーネントのサーバサイドレンダリングには対応していたのですが、Reduxコンテナ1はうまくレンダリング出来ませんでした。
そこで、この記事ではReduxコンテナのサーバサイドレンダリングに対応したHypernovaのReact/Reduxバインディングであるnoriaki/hypernova-react-reduxを紹介すると共に、シンプルなカウンターアプリをサーバサイドレンダリングで表示するまでのチュートリアルを解説します。
前回記事のチュートリアルを完了している前提で、変更点を追いかけるかたちで書いていきますので、まだの方は先に試してみてください。
Hypernova/ReactRedux
airbnb製のHypernova Reactバインディング(airbnb/hypernova-react)を改良して、Reduxコンテナのサーバサイドレンダリングが可能になるHypernova用バインディングとして作成しました。
noriaki/hypernova-react-redux: React/Redux (react-redux) bindings for Hypernova.
Hypernova/server
やHypernova/ruby
のコードを変更することなく、Hypernova/react
を置き換えることが出来るように設計しています。
Hypernova/ReactReduxを使ったサーバサイドレンダリングのチュートリアル
Reduxを利用したカウント{アップ|ダウン}ができるカウンターアプリを実装します。
また、ES2015形式やReact JSX形式の記載ができるようbabel
によるトランスパイラも使いますが、トランスパイルはbrowserify-rails
が実行してくれますのでご安心ください。
環境・バージョン
前回記事と同様ですが再掲しておきます。
# 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の導入
前回記事のチュートリアルと同様です。
4. JavaScriptの環境設定 (package.json)
ES2015やReact JSXの変換のためにいくつかパッケージを追加しています。
前回記事のチュートリアルの続きの人
以下のコマンドを実行してパッケージを追加します。
% npm i -D babelify babel-preset-es2015 babel-preset-react babel-cli
% npm i -S hypernova-react-redux react-redux redux
"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
を実行します。
{
"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の設定
前回記事のチュートリアルと同様です。
7. Hypernova/react-reduxを使用したReduxコンテナ
ここが前回と大きく違います。前回、props
から文字列を表示するコンポーネントを実装していた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
元は適切に書き換えてください)
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を利用する)場合
<%= render_react_component('MyComponent.js') %>
RailsからState情報を渡す場合
この場合カウンターの初期値が10
になります。
<%= render_react_component('MyComponent.js', count: 10) %>
8.2. Babelによるトランスパイル設定
(7)のReduxコンテナをES2016やReact JSXの記法で実装したので、browserify-rails
がこの記法を変換できるように以下を追記します。
# browserify-rails with babelify (es2015, react)
config.browserify_rails.commandline_options =
"-t [ babelify --presets [ es2015 react ] ]"
9. 動作を確認してみる
Railsアプリを起動します。
% bin/rails server
起動したら、ブラウザで http://localhost:3000/welcome/index へアクセスし、カウント: 0回
などと表示され、++
や--
のボタンクリックでカウント表示が増減することが確認できるでしょう。
ページのソースを確認してみると、該当部分は以下のようになっていると思います。
<div data-hypernova-key="MyComponentjs"></div>
<script type="application/json" data-hypernova-key="MyComponentjs"><!--{}--></script>
前回も出てきましたが、これはHypernova/server
を起動していないので サーバサイドレンダリングを利用していない アプリとして動作しています。
サーバサイドレンダリング機能を有効にする
Hypernova/server
サービスを起動すると、他のコードは変更しなくてもサーバサイドレンダリングが有効になります。
今回はbabel
によるトランスパイルが必要になっているので、サービス起動のためのnpmスクリプトを設定し利用します。具体的にはpackage.json
に以下を追加します(diffによる差分)。
"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
というコマンドが使用出来るようになります。
% 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にコンポーネントが出力され サーバサイドレンダリングを利用している ことが分かります。
<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を使った、シンプルなカウンターアプリをサーバサイドレンダリングで表示するまでのチュートリアルを解説しました。
Hypernova
を使えば、Rails
でデータベース接続やAPIを実現し、フロントエンドはReact
/Redux
によるシングルページアプリケーションを構築することも出来るでしょう。
ぜひ試してみてください。
もしお役に立ちそうなら「ストック」してもらえると嬉しいです。
なお、noriaki/hypernova-react-reduxでは、バグ発見や要望、プルリクエストなどをお待ちしています。
参考
-
Reduxでは、Store(すべてのStateを管理する単一のオブジェクト)を参照できる接続されたコンポーネント(Connected Component)をコンテナ(Container)と呼びます ↩
-
redux
パッケージのcreateStore
関数を内部で実行し、その結果を返す関数です http://redux.js.org/docs/basics/Store.html ↩