--- title: react_on_railsとreact-routerでシングルページアプリケーションをサーバサイドレンダリングする tags: Rails react-router react_on_rails React author: nukosuke slide: false --- ## :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 ( {routes} ); }; // 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 ( ); }; 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`の3つが出力されるようになった。さらに、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 <%= server_render_js("Helmet.rewind().title.toString()") %> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= yield %> <%= javascript_include_tag 'application' %> ``` ## 起動 ```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のシリアライズに使いたい。 - [ ] テストの知見を貯めたい。