読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでReact.jsをサーバーレンダリングする

Javascript React.js

f:id:nipe880324:20151122001321p:plain:w420

react-railsというReact.jsをRailsに簡単に統合できるgemを使い、React.jsをサーバーレンダリングする方法を説明します。
サーバーレンダリングすることで、初期値をHTMLの初期レンダー時に渡せるようになるので、ロード中による画面のばたつきや、ロード中といった表示をなくせます。

RailsでReactを使ってメッセージボックスアプリを作成 - Rails Webookをベースに説明しています。

また、この記事のソースコード
https://github.com/nipe0324/rails_samples/tree/master/react_server_rendering_test
です。

動作確認


1. RailsでReactコンポーネントをレンダーする

react-railsは、react_componentというビューヘルパーを用意しており、ビュー上でこのメソッドを使うことで、Reactコンポーネントのレンダーをバインディングなしでできるようになります。

main.js.jsxでのReactコンポーネントのレンダーしているReactDOM.renderを削除します。react_componentビューヘルパーのおかげでこれが必要なくなります。

// app/assets/javascripts/main.js.jsx
- $(function() {
-   ReactDOM.render(
-     <MessageBox url="/messages"/>,
-     document.getElementById('content')
-   );
- });

react_componentを使うように修正します。

<!-- app/views/top/index.html.erb -->

  <h1>Message Box</h1>

- <div id="content"></div>
+ <%= react_component('MessageBox', url: '/messages') %>

最後に、application.jsreact_ujsがあることを確認します。
react_ujsがページをスキャンをし、react_componentの箇所に指定したReactコンポーネントをマウントしています。

// app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require react
//= require react_ujs
//= require_tree .


画面を確認するとちゃんとメッセージ一覧が表示されます。
[f:id:nipe880324:20151122000620p:plain:420]


HTMLは次のようになっています。

<div data-react-class="MessageBox" data-react-props="{&quot;url&quot;:&quot;/messages&quot;}">
  ...
</div>

Turbolinksと一緒に使う場合や、コンポーネント名が階層構造になっているなどの場合などは公式ドキュメントを参照ください。
reactjs/react-rails · GitHub




2. Reactコンポーネントをサーバーレンダリングする

今度は、サーバーサイドでReactコンポーネントをレンダーするようにします。
react_componentビューヘルパーに、prerender: trueを渡します。

<!-- app/views/top/index.html.erb -->
  <h1>Message Box</h1>

* <%= react_component('MessageBox', { url: '/messages' }, { prerender: true }) %>


また、規約によりcomponents.jsが必要です。その中で、「Reactコンポーネント」や必要であればUnderscore.jsなどの「依存ライブラリ」を読み込むようにします。
今回は、Reactコンポーネントを配置しているcomponentsディレクトリを読み込むようにします。

// app/assets/javascripts/components.js

+ //= require_tree ./components


そして、application.jsでこのcomponents.jsを読み込むように修正します。

// app/assets/javascripts/application.js

  //= require jquery
  //= require jquery_ujs
  //= require react
+ //= require components
  //= require react_ujs
  //= require_tree .


公式ドキュメントにComponent Specs and Lifecycle | Reactに書かれていますが、componentDidMountはサーバーサイドでレンダリングした時には呼ばれないので、propsでReactコンポーネントに渡すようにします。

<!-- app/views/top/index.html.erb -->
  <h1>Message Box</h1>

* <%= react_component('MessageBox', { url: '/messages', messages: @messages }, { prerender: true }) %>

コントローラーで@messagesインスタンス変数に値を設定します。

# app/controllers/top_controller.rb
  class TopController < ApplicationController
    def index
+     @messages = Message.all
    end
  end


componentDidMountでメッセージをサーバーから取得してましたが、上記の通り、サーバーサイドでprops経由でMessageBoxに渡すようにしたので、初期値をpropsから取得するように修正します。
また、componentDidMountも呼ばれないので削除します。
さらに、メッセージのロード中と言う状態がなくなるので合わせて削除します。

// app/assets/components/message_box.js.jsx
  var MessageBox = React.createClass({
*   getInitialState: function(props) {
*     return { messages: this.props.messages };
    },

-   componentDidMount: function() {
-     $.ajax({
-       url:      this.props.url,
-       dataType: 'json',
-       cache:    false,
-       success: function(messages) {
-         this.setState({ messages: messages, isLoading: true });
-       }.bind(this),
-       eror: function(_xhr, status, err) {
-         console.error(this.props.url, status, err.toString());
-       }.bind(this)
-     });
-   },

    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)
      });
    },

    render: function() {
      var messageItems = this.state.messages.map(function(message) {
        return (
          <MessageItem key={message.id} message={message}/>
        );
      });

-     if (this.state.isLoading) {
-       return (
-         <div>ロード中</div>
-       );
-     } else {
      return (
        <div className="messageBox">
          {messageItems}
          <MessageForm onMessageSubmit={this.handleMessageSubmit}/>
        </div>
      );
-     }
    }
  });

画面を確認するとメッセージ一覧が表示されます。
f:id:nipe880324:20151122000620p:plain:w420


注意点として、react_componentでレンダーするReactコンポーネント群のrenderメソッドdocumentを参照できません。そのため、jQueryやライブラリなどいくつか動かない場合があります。サブミット時に$.ajaxなどはつかえます。どこらへんまで使用可能かはよくわかってないですが、うまくいかないときはこれを疑ってください。

また、therubyracerを使ったほうがパフォーマンスがいいなど、サーバーレンダリングの設定はreactjs/react-rails · GitHubを参照してください。



3. コントローラーでReactコンポーネントをレンダーする

おまけで、使う頻度は少ないと思いますが、コントローラーから直接Reactコンポーネントをレンダーできます。

// app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
    @messages = Message.all
    render component: 'MessageBox', props: { url: '/messages', messages: @messages }
  end
end

まとめ

RailsでReactコンポーネントをレンダーする

  1. react_componentメソッドでReactコンポーネントをレンダーする
  2. react_ujsが必要(react_ujsがマウントしてくれる)

サーバーサイドレンダリングを行う

  1. react_componentメソッドprerender: trueをつける
  2. react_ujsが必要(react_ujsがマウントしてくれる)
  3. components.jsが必要
  4. documentが使えない
  5. サーバーサイドレンダリングでは幾つかライフサイクルの呼ばれないメソッドがある。詳細:Component Specs and Lifecycle | React

以上です。