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

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

## :gem: react_on_rails
rails、react間のインテグレーションを実現するgemは[react-rails](https://github.com/reactjs/react-rails)っていうのと[react_on_rails](https://github.com/shakacode/react_on_rails)っていうのがある。react-railsの方が情報量が多かったけど、`application.js`で

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

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

```rb
gem 'react_on_rails', '~> 6'
```

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

```bash
bundle exec rails g react_on_rails:install
```

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

[ここ](http://r7kamura.hatenablog.com/entry/2016/10/10/173610)から拝借したコード。下記のコードはRouterコンポーネントをレンダリングする。まだそのコンポーネントを登録してないので後で`{window,global}.ReactOnRails`に登録しないといけない。

```rb
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を足しとく
```bash
# in client/
yarn add react-router react-helmet
```

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

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

```js: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して共有している。

```js: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の設定も書き換えておく。

```js: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.js`、`app-bundle.js`、`server-bundle.js`の３つが出力されるようになった。さらに、react_on_railsの設定ファイルも書き換えておく。さっき分けたサーバサイド用のjsファイルをExecJSで実行するように指定する。

```rb: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の苦しみ、こんにちは。

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

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

```erb: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>
```

## 起動
```bash
# in app root dir
foreman start -f Procfile.dev
```

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

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

- [Using React Router | react on rails](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/react-router.md)
- [react-webpack-rails-tutorial](https://github.com/shakacode/react-webpack-rails-tutorial)
- [Ruby on Rails on React on SSR on SPA | r7kamura](http://r7kamura.hatenablog.com/entry/2016/10/10/173610)

## :tada: それなりに動くようになったもの
- https://github.com/nukosuke/rails-react-template

## :dango: 今後やりたいこと
- [ ] 今のところdeviseがSPA構成をガン無視していてつらい。コントローラとかビューをreact_componentヘルパ向けに書き直す必要がありそう。
- [ ] ブラウザに配信するjsの出力先を/publicにしてsproketsを使わないようにする。jqueryとかもnpmを使う。
- [x] jbuilderを投げ捨ててrails-apiが搭載のactive_model_serializersをpropsのシリアライズに使いたい。
- [ ] テストの知見を貯めたい。