Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
9
Help us understand the problem. What is going on with this article?
@nukosuke

react_on_railsとreact-routerでシングルページアプリケーションをサーバサイドレンダリングする

More than 3 years have passed since last update.

:angry: やりたいこと

rails + react + react-router (ゆくゆくは + redux)な環境でサーバサイドレンダリング可能なシングルページアプリケーションを作成したい。自分でも「何言ってんだお前」って感じだけど、やりたいんです。

:gem: react_on_rails

rails、react間のインテグレーションを実現するgemはreact-railsっていうのとreact_on_railsっていうのがある。react-railsの方が情報量が多かったけど、application.js

//= require react
//= require react_ujs
//= require components

ってぶちまける感じが好きじゃないのでちょっといじって投げ捨てました。そういうわけでreact_on_railsを使います。

gem 'react_on_rails', '~> 6'

インストールする。未コミットの差分があるとインストールできないので注意。

bundle exec rails g react_on_rails:install

:seedling: コントローラにReactコンポーネントをレンダリングするメソッド生やす。

ここから拝借したコード。下記のコードはRouterコンポーネントをレンダリングする。まだそのコンポーネントを登録してないので後で{window,global}.ReactOnRailsに登録しないといけない。

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def home
    render_for_react()
  end

  private

  # ref: http://r7kamura.hatenablog.com/entry/2016/10/10/173610
  def common_props
    {
      currentUser: current_user,
    }
  end

  def render_for_react(props: {}, status: 200)
    if request.format.json?
      response.headers["Cache-Control"] = "no-cache, no-store"
      response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
      response.headers["Pragma"] = "no-cache"
      render(
        json: common_props.merge(props),
        status: status,
      )
    else
      render(
        html: view_context.react_component(
          "Router",
          prerender: true,
          props: common_props.merge(props).as_json,
        ),
        layout: true,
        status: status,
      )
    end
  end
end

:package: npmを足しとく

# in client/
yarn add react-router react-helmet

{server,client}Registration.jsx

それぞれサーバサイドとブライザで実行するjsをwebpackでバンドルする際のエントリファイル。
react-router使わないならデフォルトで生成されるregistration.jsxだけでいいんだけど、使う場合はそれぞれ別のRouterを用意する必要があった。この辺で頭がおかしくなってコードが釈迦になりそう。

こっちはブラウザ用のコード。

client/app/bundles/Application/startup/clientRegistration.jsx
import React from 'react';
import { Router as ReactRouter, browserHistory } from 'react-router';
import ReactOnRails from 'react-on-rails';
import routes from '../routes/routes';

const Router = (props, railsContext) => {
  const history = browserHistory;
  return (
    <ReactRouter history={history}>
      {routes}
    </ReactRouter>
  );
};

// This is how react_on_rails can see the Router in the browser.
ReactOnRails.register({
  Router,
});

こっちはサーバサイドレンダリング用のコード。routes.jsxは両方でimportして共有している。

client/app/bundles/Application/startup/serverRegistration.jsx
import React from 'react';
import { match, RouterContext } from 'react-router';
import ReactOnRails from 'react-on-rails';
import Helmet from 'react-helmet';
import routes from '../routes/routes';

// for header title server side rendeting on first load
// ref: http://r7kamura.hatenablog.com/entry/2016/10/10/173610
global.Helmet = Helmet;

const Router = (props, railsContext) => {
  let error;
  let redirectLocation;
  let routeProps;
  const { location } = railsContext;

  match({ routes, location }, (_error, _redirectLocation, _routeProps) => {
    error = _error;
    redirectLocation = _redirectLocation;
    routeProps = _routeProps;
  });

  if (error || redirectLocation) {
    return { error, redirectLocation };
  }

  return (
    <RouterContext {...routeProps} />
  );
};

ReactOnRails.register({
  Router,
});

webpackの設定も書き換えておく。

client/webpack.config.js
/**
 * from
 */
entry: [
    'es5-shim/es5-shim',
    'es5-shim/es5-sham',
    'babel-polyfill',
    './app/bundles/Application/startup/registration',
],
output: {
    filename: 'webpack-bundle.js',
    path: '../app/assets/webpack',
},


/**
 * to
 */
entry: {
    vendor: [
      'es5-shim/es5-shim',
      'es5-shim/es5-sham',
      'babel-polyfill',
    ],
    app: [
      './app/bundles/Application/startup/clientRegistration',
    ],
    server: [
      './app/bundles/Application/startup/serverRegistration',
    ]
},
output: {
    filename: '[name]-bundle.js',
    path: '../app/assets/webpack',
},

これでvendor-bundle.jsapp-bundle.jsserver-bundle.jsの3つが出力されるようになった。さらに、react_on_railsの設定ファイルも書き換えておく。さっき分けたサーバサイド用のjsファイルをExecJSで実行するように指定する。

config/initializers/react_on_rails.rb
# from
config.webpack_generated_files = %w( webpack-bundle.js )
# to
config.webpack_generated_files = %w( app-bundle.js server-bundle.js vendor-bundle.js )

# from
config.server_bundle_js_file = "webpack-bundle.js"
# to
config.server_bundle_js_file = "server-bundle.js"

turbolinksを殺す

react-routerでトランジションするSPAという状況下で、いらない子になったturbolinksを殺す。さようなら。お前に苦しめられた日々、忘れないよ。
Universalの苦しみ、こんにちは。

app/assets/javascripts/application.js
//= require vendor-bundle
//= require app-bundle
//= require jquery
//= require jquery_ujs

もちろんGemfileからも抹殺する。それからlayoutを書き換える。

app/views/layout/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <%= server_render_js("Helmet.rewind().title.toString()") %>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag 'application', media: 'all' %>
  </head>
  <body>
    <%= yield %>
    <%= javascript_include_tag 'application' %>
  </body>
</html>

起動

# in app root dir
foreman start -f Procfile.dev

ブラウザのインスペクタを開いてconsoleにエラー吐いてなかったら多分大丈夫。ちゃんとなってればreact-routerLinkコンポーネントで遷移した時と、URL直だたきのときで全く同じページが表示されるはず。
違うページが表示されたりエラーが出る場合は、だいたいcontrollerを作ってなかったとか、routes.rbでマウントし忘れてたとかいうオチ。

:book: 参考になったページとか

:tada: それなりに動くようになったもの

:dango: 今後やりたいこと

  • 今のところdeviseがSPA構成をガン無視していてつらい。コントローラとかビューをreact_componentヘルパ向けに書き直す必要がありそう。
  • ブラウザに配信するjsの出力先を/publicにしてsproketsを使わないようにする。jqueryとかもnpmを使う。
  • jbuilderを投げ捨ててrails-apiが搭載のactive_model_serializersをpropsのシリアライズに使いたい。
  • テストの知見を貯めたい。
9
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
nukosuke
オフトゥン大好き
mixi
全ての人に心地よいつながりを

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
9
Help us understand the problem. What is going on with this article?