react-railsというReact.jsをRailsに簡単に統合できるgemを使い、React.jsについて説明します。
次のような画面をReact.jsで実装し、Reactとサーバ(Rails)間でメッセージ一覧の取得や作成をできるようにします。
ソースコードはこちらです。 https://github.com/nipe0324/rails_samples/tree/master/react_test
目次
動作確認
- Ruby 2.2.0
- Rails 4.2.3
- react-rails 1.4.2
- react 0.14.2
0. React.jsとは
React.jsはMVCフレームワークでいう、ビューのみを扱っています。Backbone.jsやAngularJS、React.jsなど触ってきた私の個人的な感想として、
- React.jsは、状態をルートの一箇所(※1)で管理し、その状態を更新すれば、自動的に全てのコンポーネントをレンダーしてくれます。
- 状態を一箇所で管理できるので、レンダーの細かな制御が必要なくなるので可読性や保守性は高いと思います。また、React.jsが差分だけレンダーするのでパフォーマンスも悪くはなりにくいです。
※1: 基本的には状態をルートの1箇所で管理するとよいと思いますが、ルートのコンポーネントがFatになってしまうので大規模化するとつらくなりそうです。Fluxで解決できるかもですが、Flux実装が乱立しているのでどうしようか迷っています。Flux実装で最近よく聞くReduxを軽く触ってみようかと思います。
- Backbone.jsの場合、モデルやコレクションの更新やクリックイベントなどをトリガーとして、細かく分けたビューを更新します。ビューが大きくなり大規模化していくと、異なるビュー同士でイベントが絡み合うので、状態が増えて、つらくなってくる気がします。
- AngularJSの場合、個人的に嫌いじゃないですが、
ng-xxx
というディレクティブがたくさん必要になり覚えることがたくさんあるのでつらいかなと思います。また、Angularに処理がラッピングされているので、バグを踏んだ時に対応するのがなかなか難しい気がします。
1. react-railsのインストール
react-rails
はRailsでReact.jsとJSXを簡易に使えるようにするgemです。プロジェクトを作成します
rails new react_test cd ./react_test
react-rails
をGemfileに追加します。
# Gemfile + gem 'react-rails', '~> 1.4.0'
gemをインストールします。
bundle install
applicaiton.js
にreact
を追加し、RailsがReact.jsを読み込むようにします。
// app/assets/javascripts/application.js //= require jquery_ujs - //= require turbolinks + //= require react + //= require react_ujs //= require_tree .
React.jsの環境値を設定します。
# config/environments/development.rb Rails.application.configure do ... + # Reactの環境値を development にする + config.react.variant = :development end # config/environments/production.rb Rails.application.configure do ... + # Reactの環境値を production にする + config.react.variant = :production end
補足
今回は入れないですが、ReactにはAddonと言うものが付いていて、Addonも使いたい場合は、下記の設定をします。
# config/environments/(development|test|production).rb Rails.application.configure do ... # React.jsのAddonを有効にする(デフォルトはfalse) config.react.addons = true end
TopController#indxを作成します。
rails g controller top index
ルートのパス(/)にtop#indexを設定しておきます。
# config/routes.rb + root 'top#index'
http://localhost:3000にアクセスし、トップJavascriptコンソールにエラーがでないことを確認します。
2. Reactのコンポーネントの表示
まずはReactでHello react with rails
を表示しています。
// app/assets/javascripts/main.js.jsx + $(function() { + ReactDOM.render( + <h1>Hello react with rails</h1>, + document.getElementById('content') + ); + });
ReactDOM.render(
※少し前まではReact.renderでした。
がReactのコンポーネントとして認識されて表示されます。Hello react with rails
他にも、divやspanなど基本的なHTML要素は利用できます。
<!-- app/views/top/index.html.erb --> + <h1>Message Box</h1> + + <div id="content"></div>
画面を再表示すると次のように「Hello react with rails」とReactにより表示されます。
3. メッセージボックスの概要
ReactはVirtual DOMツリーを使って差分のみのHTMLを更新します。
ルートノードがstate(変更可能)を持っていて、必要な子ノードにstate(変更不可)を渡します。
子は親から渡された値をprops(変更不可)として受け取り、それを使ってHTMLをレンダーします。
各子ノードはイベントが発生したら親までイベントを伝え、親のstate(変更可能)をsetStateメソッドで更新します。
すると、親ノードから全てのノードが再度更新されます。この時、Virtual DOMツリーがあるためHTMLを差分だけ更新します。
次のようなツリー構造を作っていきます。
MessageBox L MessageList | L MessageItem L MessageForm
4. メッセージボックスのリストを作成
では、ここからメッセージボックスを作成していきます。まずは、Reactのコンポーネントを配置するディレクトリを作成します。
mkdir app/assets/javascripts/components
MessageBoxコンポーネントの作成
MessageBoxコンポーネントを作成します。// app/assets/javascripts/components/message_box.js.jsx + var MessageBox = React.createClass({ + render: function() { + return ( + <div className="messageBox"> + This is message box. + </div> + ); + } + });
React.createClass()
でコンポーネントを作成します。
また、コンポーネントが表示するHTMLをrender
の関数で返します。
classが予約されているので、HTMLのclass属性の指定はclassName
を使います。
main.js.jsxからMessageBoxコンポーネントを呼び出すように修正します。
// app/assets/javascripts/main.js.jsx $(function() { ReactDOM.render( * <MessageBox />, document.getElementById('content') ); });
画面を確認すると次のようになります
ReactDOM.render
でレンダリングしているHTMLは次のようになっています。
MessageBoxコンポーネントのrender
のHTMLが表示されていることがわかると思います。
<div id="content"> <div class="messageBox" data-reactid=".0"> This is message box. </div> </div>
※data-reactid
は、Reactが各DOMを管理するために自動的に付与しているデータです。
Reactのrender
メソッドの実装時にはまりやすいポイント
render
で返すトップのHTML要素は1つでないといけません。次のように、div要素を2つ返そうとするとエラーになります。
var MessageBox = React.createClass({ render: function() { // 2つのdiv要素を返しているのでエラーになる return ( <div className="messageBox"> This is message box. </div> <div className="messageBox"> This is message box. </div> ); } });
また、inputやimageなどの要素でも必ず閉じタグが必要です。
var MessageBox = React.createClass({ render: function() { return ( <div className="messageBox"> <image src="/path/to/file"/> {/* 正しい */} <image src="/path/to/file"> {/* エラー */} </div> ); } });
MessageItemコンポーネントの作成
各メッセージを表示するMessageItemコンポーネントを作成します。MessageBoxコンポーネントを作成します。
// app/assets/javascripts/components/message_item.js.jsx +var MessageItem = React.createClass({ + render: function() { + return ( + <div className="message"> + <h2 className="messageUser">{this.props.message.user}</h2> + <span>{this.props.message.text}</span> + </div> + ); + } +});
this.props
には、親コンポーネントから渡された値が入ります。
また、{変数名}
で変数の値を表示することができます。
ここでは、親からmessage = { user: 'username', text: 'text' }
といったような値が渡されて、{}
を使ってそれぞれユーザ名とメッセージのテキストを表示しています。
message_box.js.jsxでMessageItemコンポーネントを作成し、returnで返すようにします。
// message_box.js.jsx var MessageBox = React.createClass({ render: function() { + var messageItems = this.state.messages.map(function(message) { + return ( + <MessageItem key={message.id} message={message}/> + ); + }); return ( <div className="messageBox"> + {messageItems} </div> ); } });
this.state
は変更可能な値で名前からわかる通りアプリの状態を保持します。
基本的にはルートのノード(MessageBox)だけでstate
を管理すると、いろいろなコンポーネントに状態が散らばらないので分かり易い、保守し易いJSコードになります。
そして、this.setState
メソッドを通してstate
を更新することでルート以下のコンポーネントのrender
が呼ばれ、state
の状態を表せます。この時、Reactが差分だけ更新するのでパフォーマンスをあまり気にしなくてもrenderが行えるようになっています。
最終的にはサーバーからデータを取得しますがまずは仮で、初期データを用意します。
getInitialState
はコンポーネントが作成された時に1度だけ呼ばれ、state
の初期値を返すように実装します。
// message_box.js.jsx var MessageBox = React.createClass({ + getInitialState: function() { + return { + messages: [ + { id: 1, user: 'Tom', text: 'Good morning' }, + { id: 2, user: 'John', text: 'Good afternoon' }, + { id: 3, user: 'Emily', text: 'Good evening' } + ] + }; + }, render: function() { ...
では、画面をリロードします。
ちなみにHTMLは次のようになっています。
<div id="content"> <div class="messageBox" data-reactid=".0"> <div class="message" data-reactid=".0.$1"> <h2 class="messageUser" data-reactid=".0.$1.0">Tom</h2> <span data-reactid=".0.$1.1">Good morning</span> </div> <div class="message" data-reactid=".0.$2"> <h2 class="messageUser" data-reactid=".0.$2.0">John</h2> <span data-reactid=".0.$2.1">Good afternoon</span> </div> <div class="message" data-reactid=".0.$3"> <h2 class="messageUser" data-reactid=".0.$3.0">Emily</h2> <span data-reactid=".0.$3.1">Good evening</span> </div> </div> </div>
5. メッセージフォームを作成
メッセージを入力するフォームのMessageFormコンポーネントを作成し、メッセージを投稿できるようにします。MessageFormコンポーネントの作成
まずはイベントなどないシンプルなMessageFormコンポーネントを作成します。// app/assets/javascripts/components/message_form.js.jsx + var MessageForm = React.createClass({ + render: function() { + return ( + <form className="commentForm"> + <input type="text" placeholder="Yousr name" /> + <input type="text" placeholder="Message" /> + <input type="submit" value="Post" /> + </form> + ); + } + });
MessageBoxからMessageFormをレンダーするように修正します。
// message_box.js.jsx var MessageBox = React.createClass({ getInitialState: function() { ... }, render: function() { var messageItems = this.state.messages.map(function(message) { return ( <MessageItem key={message.id} message={message}/> ); }); return ( <div className="messageBox"> {messageItems} + <MessageForm /> </div> ); } });
画面を確認するとFormが表示されます。
Submitイベントのハンドリング
Postボタンを押した時に、動的にMessageItemを追加するように修正します。まずは、MessageFormにイベントのハンドリングを行うhandleSubmit
を追加します。
var MessageForm = React.createClass({ + handleSubmit: function(event) { + event.preventDefault(); + var user = this.refs.user.value.trim(); + var text = this.refs.text.value.trim(); + // どちらか入力されてなければ何もしない + if (!user || !text) { + return; + } + // 親コンポーネントのMessageBoxのイベントを呼ぶ + this.props.onMessageSubmit({ user: user, text: text }); + // フォームの内容を削除 + this.refs.user.value = ''; + this.refs.text.value = ''; + }, render: function() { return ( * <form className="commentForm" onSubmit={this.handleSubmit}> * <input type="text" placeholder="Yousr name" ref="user" /> * <input type="text" placeholder="Message" ref="text"/> <input type="submit" value="Post" /> </form> ); } });
まずonSubmit={this.handleSubmit}
でSubmitイベントが発生したら、handleSubmitが呼ばれるように定義しています。
Postボタンを押すとリストに入力内容が追加されます。
keyについて
Postボタンでもう一つメッセージを追加するとJavascriptコンソールに次のようなメッセージが表示されます。react.self-bf407d87....js?body=1:2166 Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `MessageBox`. See https://fb.me/react-warning-keys for more information.
和訳すると、
> 配列やイテレータのそれぞれの子要素はユニークな"key"属性をつけてください。
> "MessageBox"のrenderメソッドのところが怪しいです。
> より詳細を知りたい場合は、https://fb.me/react-warning-keys を読んでください。
です。
なぜ、Reactで複数の同じ子要素(liやReactのコンポーネントなど)を表示するときにkey属性を指定しないといけないかは、
Reactのstateが変わったときにどの要素を更新すればいいか識別するために使われています。key属性がなくてもうまく更新ができますが、Reactがいい感じにしないといけず計算量が多くなってしまいパフォーマンスが悪くなってしまう基本的にはkey属性をつけます。
今回は、MessageItemがリスト要素になっているので、key属性に値を設定します。
すでに設定していますが、新たにフォームから追加したメッセージはidがないので、handleMessageSubmit
ないでユニークなidを生成するようにします。
※後ほどサーバーから取得したメッセージを取得するようにしますが、今はidを生成する仕組みがないので暫定で日付を設定するようにします。
// message_box.js.jsx var MessageBox = React.createClass({ getInitialState: function() { ... }, handleMessageSubmit: function(message) { + message.id = new Date(); var newMessages = this.state.messages.concat(message); this.setState({ messages: newMessages }); }, render: function() { var messageItems = this.state.messages.map(function(message) { return ( <MessageItem key={message.id} message={message}/> ); }); return ( <div className="messageBox"> {messageItems} <MessageForm onMessageSubmit={this.handleMessageSubmit}/> </div> ); } });
これでワーニングが出なくなりました。
6. サーバーサイド(Rails)との連携
RailsでAPIを作成
APIのエンドポイントとして下記2つを用意します。
GET /messages.json - メッセージの一覧を取得する POST /messages.json - メッセージを作成する
まず、コントローラ、モデルを作成します。
rails g resource Message user:string text:string
※railg g resource
は、モデル、マイグレーションファイル、コントローラ、アセット、ヘルパーを作成(ビューを作成しない)
ルートをエンドポイントの2つだけにします。
# config/routes.rb Rails.application.routes.draw do * resources :messages, only: [:index, :create], format: 'json' root 'top#index' end
MessageControllerでindex
とcreate
メソッドを実装します。
# app/controllers/messages_controller.rb + class MessagesController < ApplicationController + def index + messages = Message.all + render json: messages + end + + def create + message = Message.new(create_params) + if message.save + render json: message, status: :created # 201 + else + render json: message, status: :unprocessable_entity + end + end + + private + + def create_params + params.permit(:user, :text) + end + end
初期データを作成し、投入しておきます。
# db/seeds.rb Message.delete_all Message.create!([ { user: 'Tom', text: 'Good morning' }, { user: 'John', text: 'Good afternoon' }, { user: 'Emily', text: 'Good evening' } ]) # terminal bundle exec rake db:migrate db:seed
サーバー再起動して、http://localhost:3000/messages.jsonにアクセスすると次のように値が返ってくると思います。
[{"id":1,"user":"Tom","text":"Good morning","created_at":"2015-11-19T04:02:27.956Z","updated_at":"2015-11-19T04:02:27.956Z"},{"id":2,"user":"John","text":"Good afternoon","created_at":"2015-11-19T04:02:27.958Z","updated_at":"2015-11-19T04:02:27.958Z"},{"id":3,"user":"Emily","text":"Good evening","created_at":"2015-11-19T04:02:27.962Z","updated_at":"2015-11-19T04:02:27.962Z"}]
Reactでサーバからメッセージ一覧を取得
メッセージの一覧を取得するエンドポイントは、GET /messages.json
なので、Ajaxでサーバからメッセージを取得して、それをReactで表示するようにしてみます。まずは、MessageBoxにAPIへのエンドポイントとなるurl属性を渡します。
// app/assets/javascripts/main.js.jsx $(function() { ReactDOM.render( * <MessageBox url="/messages"/>, document.getElementById('content') ); });
次にgetInitialState
の仮で用意していた初期値を空の配列にします。
また、componentDidMount
を定義し、その中でサーバーからメッセージ一覧を取得し、setState({ messages: messages })
でstateにメッセージを設定します。
componentDidMount
は、コンポーネントが表示された時にReactによって自動的に呼ばれるメソッドです。
// app/assets/javascripts/components/message_box.js.jsx var MessageBox = React.createClass({ getInitialState: function() { * return { messages: [] }; }, + componentDidMount: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(messages) { + this.setState({ messages: messages }); + }.bind(this), + eror: function(_xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, ... });
画面を確認するとメッセージ一覧が表示されます。
この時若干画面がバタつくのは、次のようになっているからです。
getInitialState
でmessagesに空の配列がセットされる。- Reactが
MessageBox
を表示する。この時、メッセージがないので一覧は表示されない。 MessageBox
が表示されたので、componentDidMount
が呼ばれ、サーバからメッセージを取得し、messagesにメッセージをセットする。setState
でmessagesが更新されたので、Reactは差分をレンダーする。この時、メッセージがあるので一覧は表示される。
このように、一度なにも表示しないで、その後、サーバーからメッセージを取得して、一覧を表示するので若干バタつくようになっています。
ロード中と表示
ロード中に「ロード中」と表示するようにします。うまくスタイリングすれば、「getInitialState〜サーバーからのデータ取得」をまでの間のバタつくような表示を解消できます。具体的には、次のようにロード中と一瞬だけ出てから一覧が表示されるようにします。

MessageBoxを修正し、isLoading
というステータスをもたせて、ロードが完了した時点でロードが完了したと設定しています。
また、そのisLoading
の値を見て、レンダーする表示を変えています。
// app/assets/javascripts/components/message_box.js.jsx var MessageBox = React.createClass({ getInitialState: function() { // isLoading = true : ロード中を表示 * return { messages: [], isLoading: true }; }, componentDidMount: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(messages) { // isLoading = false : ロード中を表示しない * this.setState({ messages: messages, isLoading: false }); }.bind(this), eror: function(_xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, handleMessageSubmit: function(message) { ... }, render: function() { var messageItems = this.state.messages.map(function(message) { return ( <MessageItem key={message.id} message={message}/> ); }); // isLoadingの値により表示するRenderする内容を変えている + if (this.state.isLoading) { + return ( + <div>ロード中</div> + ); + } else { * return ( * <div> * <h1>Message Box</h1> * <div className="messageBox"> * {messageItems} * <MessageForm onMessageSubmit={this.handleMessageSubmit}/> * </div> * </div> * ); + } } });
このローディング中という表示が嫌な場合は、、サーバー側でレンダリングするデータも含めて返すようにする「サーバーレンダリング」という方法があります。そちらについては、次の記事で書きます。
ReactでサーバへメッセージをPOSTする
メッセージを作成するエンドポイントは、POST /messages.json
なので、フォームのPostボタンが押された時にAjaxでサーバーにメッセージ情報をPOSTするようにします。MessageBoxのhandleSubmit
でサーバー側にフォームに入力されたメッセージ情報をPOSTするようにします。
// app/assets/javascripts/components/message_box.js.jsx var MessageBox = React.createClass({ ... + handleMessageSubmit: function(message) { + $.ajax({ + url: this.props.url, + dataType: 'json', + type: 'POST', + data: message, + success: function(message) { * var newMessages = this.state.messages.concat(message); this.setState({ messages: newMessages }); + }.bind(this), + error: function(_xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, ... }
確認
投稿できるようになります。
以上です。
サーバーサイドでReact.jsをレンダリングする「サーバーレンダリング」について記載しましたので、ロード中による画面のばたつきや、ロード中といった表示をなくしたい場合は参考にしてください。
RailsでReact.jsをサーバーレンダリングする - Rails Webook