Rails アプリに React.js を導入する react-rails を試してみる
react-rails という、React.js を Rails 上で使いやすくしてくれている Gem があったので入れてみる。
インストール
任意の Rails アプリの Gemfile
に、以下を書く。
# React Rails
gem 'react-rails', '~> 1.0'
そしたら bundle install
を実行してインストールする。
$ bundle install
…(省略)…
Installing react-rails 1.10.0
…(省略)…
次に React.js を Rails アプリにインストールするスクリプトを実行する。
$ rails generate react:install
create app/assets/javascripts/components
create app/assets/javascripts/components/.gitkeep
insert app/assets/javascripts/application.js
insert app/assets/javascripts/application.js
insert app/assets/javascripts/application.js
create app/assets/javascripts/components.js
components
ディレクトリが作られたり、application.js
に何か書き加えられたらしき結果が出ている。少し見てみる。
application.js
に以下が追加されている。
//= require react
//= require react_ujs
//= require components
ココで読み込まれている「components
」は、同ディレクトリの components.js
のことらしい。中を見てみると以下のようになっている。
//= require_tree ./components
要するにさっき作られた、今のところまだ空の components
ディレクトリ配下のファイルを全て読み込む、という話らしい。
React コンポーネントの作成
では、React コンポーネントを作ってみる。
React コンポーネントを生成してくれるコマンドがあるのでコレを使う。
$ rails generate react:component Film
create app/assets/javascripts/components/film.js.jsx
作られた film.js.jsx
はこんな風になっている。
var Film = React.createClass({
render: function() {
return <div />;
}
});
空 div を吐くだけだけど、最初から React コンポーネントの雛形が作られている。
今回は最終的に、映画情報の一覧ページを作ってみたい。
まずは静的データを表示する
views/films/index.html.erb
の中身を以下の1行にする。
<%= react_component('Film') %>
これで、react-rails が app/assets/javascripts/components/film.js.jsx
に書かれている Film
という React のコンポーネントクラスを特定して出力してくれる。Rails の render
相当ということだろう。
先ほどの film.js.jsx
に、まずは固定文言だけ書いてみる。
var Film = React.createClass({
render: function() {
return (
<div className="film">
MyCinemaReview : React-Rails Sample.
</div>
);
}
});
こんな風に書き換えた。JSX はほとんど HTML そのままを JS コードに混ぜ込んで書くことができる。class
属性は予約語と衝突するためか、className
として書く必要がある。
あとは rails server
を起動すると、画面表示されるようになっている。
rails s
でサーバを起動した状態で react-rails のインストールやコンポーネントの追加などをしていたら「NoMethodError」となってしまったので、インストール手順に問題がない場合はサーバを一旦落として再起動してみると良いかも。
固定データを渡してデータバインディングをやってみる
index.html.erb
に以下のように書くことで、React の prop
にデータを渡せる。ここでは「films
」という名前で、映画情報の Hash を渡している。
<%= react_component('Film',
films: [
{ title: 'Test 1', release_date: '2010-01-01' },
{ title: 'Test 2', release_date: '2010-11-11' }
]
) %>
film.js.jsx
の方はこんな風にすると、films
からデータを読み取って、リストを並べて表示させることができた。
// 親コンポーネント
var Film = React.createClass({
render: function() {
return (
<div className="film">
<h1>MyCinemaReview React-Rails Sample</h1>
<FilmList films={this.props.films} />
</div>
);
}
});
// 一覧を構成する子コンポーネント
var FilmList = React.createClass({
render: function() {
// Hash の1要素を構成するまとまりを作る
var filmNodes = this.props.films.map(function(film) {
return [
<FilmListDt title={film.title} />,
<FilmListDd release_date={film.release_date} />
];
});
// 一覧を返却する
return (
<dl>
{filmNodes}
</dl>
);
}
});
// dt 要素
var FilmListDt = React.createClass({
render: function() {
return (
<dt>{this.props.title}</dt>
);
}
})
// dd 要素
var FilmListDd = React.createClass({
render: function() {
return (
<dd>
<ul>
<li>公開日 : {this.props.release_date}</li>
</ul>
</dd>
);
}
})
React.js を知らないと、「なんで dt
要素と dd
要素は filmNodes
で一気に書けないの?」と思ったと思う。ぼくもそう思った。最初以下のように書いたらエラーになってしまった。
// これは2つのタグ = コンポーネントを返しているのでエラーになる
var filmNodes = this.props.films.map(function(film) {
return (
<dt>
{film.title}
</dt>
<dd>
{film.release_date}
</dd>
);
});
要するに、return
する「ルートの要素 = コンポーネント」は1つのみという決まりがあるらしく、同階層に複数の要素があってはいけない、ということなのだ。
返却する要素が1つなら良いので、HTML としてイケてないのは承知の上で、もしも以下のように書いたとしたらエラーにはならない。
// 一覧を構成する子コンポーネント
var FilmList = React.createClass({
render: function() {
var filmNodes = this.props.films.map(function(film) {
return (
<dl>
<dt>{film.title}</dt>
<dd>{film.release_date}</dd>
</dl>
);
});
// 一覧を返却する
return (
<div>
{filmNodes}
</div>
);
}
});
これなら、FilmList
としては div
を返し、その中に連想配列の要素分だけ filmNodes
が返した dl
要素が並ぶことになる。HTML 的には dl
要素は1つでいいので、dt
要素と dd
要素をそれぞれ返す、こういう書き方になった。
- 参考 : reduxを試してみた(3日目) - redux-boilerplateを使ってひな形を生成する - Qiita
- 参考 : javascript - How to model a list in React - Stack Overflow
JSON でデータを返すコントローラを作る
JSON を返すだけのコントローラを作る。JS や CSS は不要なので、--no-assets
オプションを指定する。
$ rails g controller api/films --no-assets
create app/controllers/api/films_controller.rb
invoke erb
create app/views/api/films
invoke test_unit
create test/controllers/api/films_controller_test.rb
invoke helper
create app/helpers/api/films_helper.rb
invoke test_unit
app/assets/controllers/api/films_controller.rb
が出来ているので、以下のように実装する。さっき index.html.erb
に書いていた Hash 部分をそのまま移植した状態。
class Api::FilmsController < ApplicationController
def index
@films = [
{ title: 'Test 1', release_date: '2010-01-01' },
{ title: 'Test 2', release_date: '2010-11-11' }
]
end
end
次に app/views/api/films/index.json.jsonbuilder
を作り、以下のように実装する。
# JBuilder の中ではシャープでコメントアウトできる (Ruby としてパースするのだろう)
# コントローラから受け取った @films を1行ずつ処理している
json.data(@films) { |film| json.extract!(film, :title, :release_date) }
あとはルーティング。config/routes.rb
に以下を追加する。
# Films の JSON データを返す API のルーティング
namespace :api, format: 'json' do
resources :films
end
これで rake routes
をしてルーティングを反映させる。
$ rake routes
Prefix Verb URI Pattern Controller#Action
films GET /films(.:format) films#index
POST /films(.:format) films#create
new_film GET /films/new(.:format) films#new
edit_film GET /films/:id/edit(.:format) films#edit
film GET /films/:id(.:format) films#show
PATCH /films/:id(.:format) films#update
PUT /films/:id(.:format) films#update
DELETE /films/:id(.:format) films#destroy
# ココから下が今回追加した api/films のルーティング
api_films GET /api/films(.:format) api/films#index {:format=>/json/}
POST /api/films(.:format) api/films#create {:format=>/json/}
new_api_film GET /api/films/new(.:format) api/films#new {:format=>/json/}
edit_api_film GET /api/films/:id/edit(.:format) api/films#edit {:format=>/json/}
api_film GET /api/films/:id(.:format) api/films#show {:format=>/json/}
PATCH /api/films/:id(.:format) api/films#update {:format=>/json/}
PUT /api/films/:id(.:format) api/films#update {:format=>/json/}
DELETE /api/films/:id(.:format) api/films#destroy {:format=>/json/}
これで http://localhost:3000/api/films.json
にアクセスすると JSON データが取れるはず。
React で JSON データを受け取って表示させる
こうやって作った films.json
を、React で扱えるようにしていく。
まず React を表示させていた app/views/films/index.html.erb
で以下のように書き換える。
<%= react_component('Film', url: '/api/films.json') %>
今のところ、React には URL を渡しているだけなので、この URL にアクセスし、受け取った JSON データを子コンポーネントに受け渡すよう、親コンポーネントを直す。
ここでstateという概念が出てくる。
既に出てきているpropsは、これまで必ず親コンポーネントから渡されている。
propsの値は親Componentで管理するので、決してReactのComponentの中で変更してはいけない。
これを守ることで、ReactのComponentは同じpropsを渡される限りは、必ず同じレンダリング結果になるので、immutableなComponentとして扱えることになる。これ重要!
じゃあ親Componentでは値の変更はどうするの、というとこれはstateという、Componentの状態を保持するための変数に適用する。
ということで、app/assets/javascripts/components/film.js.jsx
の親コンポーネントを以下のように直す。
// 親コンポーネント
var Film = React.createClass({
// State の初期値設定
getInitialState: function() {
// films には空の配列を渡して初期化しておく。これが {this.state.films} でアクセスできる State になる
return {
films: []
};
},
// コンポーネントが最初にレンダリングされた時の処理
componentDidMount: function() {
// State {this.state.films} となるデータを取得する
$.ajax({
url: this.props.url,
dataType: 'json',
success: function(result) {
// State {this.state.films} をセットする
this.setState({
films: result.data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function() {
return (
<div className="film">
<h1>MyCinemaReview React-Rails Sample</h1>
<FilmList films={this.state.films} />
</div>
);
}
});
getInitialState()
と componentDidMount()
は React で決まっている function みたい。ココでそれぞれ初期値の設定と Ajax でのデータ取得とセットを行っている。
render()
内は一箇所だけ、<FilmList />
に渡すデータを {this.props.films}
から {this.state.films}
に変更している。
子コンポーネントを修正していないのが特徴。親コンポーネントでやり方を変えるだけで良い、というワケだ。
定期的にポーリングする
これだと初回アクセスでしかレンダリングされないので、定期的にポーリングするように書き換える。
まずは app/views/films/index.html.erb
でポーリングの間隔を指定する。
<%# Ajax で JSON を取得できるよう URL を渡し、ポーリングする間隔を指定する %>
<%= react_component('Film', url: '/api/films.json', pollInterval: 2000) %>
次に app/assets/javascripts/components/film.js.jsx
を以下のように書き換える。
// 親コンポーネント
var Film = React.createClass({
// State {this.state.films} となるデータを取得する
loadFilms: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
success: function(result) {
// State {this.state.films} をセットする
this.setState({
films: result.data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
// State の初期値設定
getInitialState: function() {
// 同じなので省略
},
// コンポーネントが最初にレンダリングされた時の処理
componentDidMount: function() {
// 初回読み込み
this.loadFilms();
// 指定の間隔でポーリングする
setInterval(this.loadFilms, this.props.pollInterval);
},
render: function() {
// 同じなので省略
}
});
componentDidMount()
内にいた $.ajax
部分を、適当な function として外出しした。componentDidMount()
で、初回の読み込みと、定期的なポーリングのために setInteval()
を設定している。
実際にデータを取得して表示させる
ようやく実際に DB からデータを取得するところに移る。
app/controllers/films_controller.rb
に実装していた内容を app/controllers/api/films_controller.rb
に移植してやれば良いだろう。
class Api::FilmsController < ApplicationController
# IndexAction
# JSON でデータを返す
def index
# Model の all() で全件取得する
@films = Film.all
end
end
app/controllers/api/films_controller.rb
は空にしてしまう。
class FilmsController < ApplicationController
# IndexAction
def index
# Do Nothing
end
end
これでトップページができた。
所感
既存の Rails アプリのフロントエンドを、これから React.js に移植していく、といった場面であれば、この RubyGems を使うのは効果的かもしれない。
ゼロから Rails アプリを作ろうとする時は、アプリの規模・開発形態にもよるが、この Gem を使わず、フロントエンド周りは npm で導入し、Webpack での管理に統一してやった方が、フロントエンドとバックエンドが分離できて良いかもしれない。
参考
日本語で React と Rails の組み合わせという記事になると、React 単体、Rails 単体をそれぞれよく知っている人が書いている記事が多い気がして、ぼくみたいな初心者が入門として参考にできる記事がなかなかなかった。それぞれの記事を何度も読んでやってみて、ようやくなんとかこの辺までこぎつけたという感じ。
- GitHub - reactjs/react-rails: Ruby gem for automatically transforming JSX and using React in Rails.
- react-railsを使ってReactのTutorialをやってみる - Qiita … この記事がとても参考になった。
- RailsでReact.jsをサーバーレンダリングする - Rails Webook
- Building a CRUD interface with React and Ruby on Rails (Example) | hack.guides()
- React.jsをreact-railsとwebpackそれぞれで動作させてみる | スペースマーケットブログ
- react-railsでサーバーサイドレンダリングしつつクライアントでsetStateできて最高になった - Qiita