君たちはReactをどうやってRuby on Railsに載せるべきか?
はじめに
こんにちは!Ruby on Railsフロントエンドエンジニアを目指し、Hotwireを中心に活動しつつ、Next.jsもReactも勉強している加々美です!
2025年2月14日(バレンタインデイ)に、ReactのチームはCreate React Appを公式に非推奨とするブログポストを公開しました。そして代わりにSPA用のフレームワークを使うべきだと彼らは強く主張しました。
大事なポイントは、SSRフレームワークを推奨したわけではないということです。SPAフレームワークを推奨したのです。CDNとか静的ホスティングサービスにデプロイできるSPAでもフレームワークを使いなさいということです。
「オレはSSRに興味ねぇ!SEOはどうでも良いからSPAで十分だ。Create React Appがダメなら、Viteを使えば良いさ!」
=> これは違います。 Reactチームはこういう人でもSPA用のフレームワークを使いなさいと言っています(自分で作ると大変だよという記事)。波に乗り遅れると、気づいたらレガシーReactを書いていることになるかもしれません。👴
「オレはRuby on Railsバックエンドと繋げるし、ERBファイルにjavascript_include_tag
とかjavascript_pack_tag
を使ってReactを読み込むから関係ねぇぜ!」
=> これも違います。 こんなモロ ライブラリー的な使い方はやめなさいとReactチームは言っています。代わりに〜〜ぃ... 😯 アレ?代わりになるものってなんだ??
Ruby on Railsの話に戻すと、Railsの世界では、Webpack以来ERBファイルにjavascript_include_tag
やjavascript_pack_tag
を埋め込んでReactのファイルを読み込むことが一般的でした。Vite Railsのgemでも同じやり方です。Laravelでは激アツのInertiaという技術があってこれは色々と別物ですが(Rails版もある)、普通にReactを載せるものとして用意されているMixとかViteは昔ながらのRailsスタイルと同じやり方です。
でもこのやり方は完璧にReactをライブラリとして使っているものです。Reactチームの言うSPA用フレームワークを使ったものではありません。このままだと、私たちは非推奨のやり方を続けるしかありません。私たちに必要なのは、Ruby on RailsとSPA用フレームワークを統合する(少し新しい)方法です。
ここではそのようなものを一つ提案したいと思います。 📢
今回私が用意したもの
- 私が今回提案するアプローチをベースに作ったデモアプリを、KamalでさくらのVPSに載せていいます。基本的なものですが、ぜひ触ってみてください。🧸
- デモサイトでは、サーバへのアクセスは全て人工的に2秒間の遅延を入れています。「色の白いは七難隠す」と同じ感じで、サーバが速いと雑な作りをしてもフロントエンドのボロは出にくいものです。わざと遅延を入れることで、SPA用フレームワークの良さが見えてきます(他にも色々良さはありますが)
- ソースコードはGitHubに載せています。💻
基本構成
- Reactチームが推奨したSPAフレームワークのうち、React Router v7を使っています。これをFrameworkモード、かつ
ssr: false
のSPAモードで使用しています。以前はRemixのSPAモードとも言っていたやつです。- Reactチームは他にNext.jsのStatic Exportおよび、少なくとも日本ではあまり聞かないExpoを推奨しています。Next.jsはダイナミックルートのところが腑に落ちないのと、Expoは日本でほぼ聞いたことがないので、今回はこの2つは見送りました。
- バックエンドはRuby on Rails 8です。
- 古くから良く使われている
javascript_include_tag
やjavascript_pack_tag
を含むERBファイルは一切使いません- React Routerは
ssr: false
のSPAモードの時、ブラウザが最初に読み込むbootstrap用のindex.html
ファイルを事実上SSGでビルドします。 - この
index.html
ファイルはSPAとしての各種の最適化が含まれています。SPAのUI/UXを考えると、これをこのまま使うのがベストに思えます。 - Bootstrap用のERBファイルの代わりに、この
index.html
ファイルをRails経由でそのままブラウザに送ります。 - Railsのコントローラ経由で
index.html
ファイルを提供することにより、cacheやcookie関連のHttp headerが自在に制御できます。おかげでRailsと強力に統合できます。
- React Routerは
やること
GitHubにあるソースコードでかなり細かくコメントしていますので、ここではかいつまんで説明します。
Ruby on Railsのインストール
rails new [react-router-vite-rails]
[react-router-vite-rails]はプロジェクト名です。
全くデフォルトのままなので、JavaScript側もCSS側も完全にno-buildのものになります。Nodeすらインストールされていないものから出発します。
BootstrapのHTMLテンプレートを提供するルートの用意
今回はBootstrapのHTMLテンプレート(ブラウザが最初に読み込むHTML)はRailsから提供します。そのためのrouteとcontrollerの設定です。
config/routes.rb
match "react-router/*path", to: "react#show", via: :all
よくあるCatchall routeです。https://[host]/react-router/*
のURLを全てReactController#show
で処理します。
controllers/react_controller.rb
class ReactController < ApplicationController
...
def show
render file: Rails.root.join("public", "react-router", "react-router-index.html")
end
end
ReactController#show
のようなactionからbootstrap HTMLテンプレートを出すのは一般的ですが、今回はERBテンプレートを用意しない点が普通と異なります。その代わり**public/react-router/react-router-index.html
にあるファイルをそのままブラウザに送り返しています**。
HTMLは静的ファイルをそのまま送信していますが、Railsのcontrollerの中での処理ですので、HTML header ("Cache-Control"とかcookie関連とか)はRailsが自在に設定できるのが特徴です。例えば別サーバのNext.jsにフロントをおいている場合は、こういった連携が簡単にはできません。
今回のケースでいえば、JavaScript, CSS, 画像などのassetファイルはpublic
フォルダから出力されますので、RailsのActionDispatch::Staticミドルウェアが担当しています。そしてこの時に期限の長い"Cache-Control"をつけます。ランダムなハッシュ(digest)がついているため、キャッシュの期限が長くても、バージョンを変えるとちゃんと切り替わります。
一方でbootstrap HTMLファイルに長い"Cache-Control"をつけると、Chrome系のブラウザは長期キャッシュしてしまうので(Safari系は空気を読んでキャッシュを短くします)、bootstrap HTMLファイルがちゃんとアップデートされない恐れがあります。そのため、このファイルはpublic
からではなく、別途"Cache-Control"が制御できた方が安全です。今回はこれが可能な仕組みになっています。
ここは少しややこしい話なのですが、SPAの入り口のbootstrap HTMLファイルは常にindex.html
という名前です。バージョンが変わっても同じ名前です。一方でReactを含むJavaScriptファイルはViteが例えばassets/home-BdE4zH5f.js
("BdE4zH5f"のところはバージョンを表す)などの名前に変更してくれます。bootstrap HTMLファイルにはバージョン番号がつかないため、意図せずにキャッシュのものが使われてしまう可能性があり、異なるキャッシュ設定が必要です。
またRails controllerからbootstrap HTMLファイルを送信するときにcookieを自在につけることができますので、静的なHTMLであっても最初から認証状態やテーマなどをブラウザに伝えることができます(JSON APIからステートを読み込む必要がありません)。おかげで初期ロードのチラつきが抑制できるケースもあるでしょう。
React Routerのインストール
React Routerをインストールします。
npx create-react-router@latest [frontend-react-router]
[frontend-react-router]は、React Routerをインストールするフォルダです。Railsのプロジェクトの直下に置くと良いでしょう。
[frontend-react-router]/react-router.config.js
export default {
ssr: false,
buildEnd: async () => {
await rm("../public/react-router", { recursive: true, force: true })
await rename("build/client/index.html", "build/client/react-router-index.html")
await rename("build/client", "../public/react-router")
await rm("build", { recursive: true, force: true })
},
basename: "/react-router/"
} satisfies Config;
ここではssr: false
でSPAモードに変更しています。
buildEnd
はビルドが終了した後に行われる作業です。ここではビルドして生成されたファイルを、Railsのpublic
フォルダに移動しています。その際、index.html
はブラウザから直接アクセスして欲しくないので(Rails controller経由でアクセスしてもらいたい)、react-router-index.html
と変更し、普通にはアクセスされないようにしています。(上記のReactController#show
で参照していたものです)
Ruby on Railsのasset pipelineに載せる場合は、通常はapp/assets/builds
にファイルを保存します。そうするとrails assets:precompile
の時にPropshaft (もしくはsprockets)がこれにランダムなハッシュ(digest)をつけて、public
フォルダに移動してくれます。しかし今回はReact Routerのbuildの中でdigestはすでに付与されているので、ここは飛ばして直接public
に移動させます。
frontend-react-router/vite.config.ts
...
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
ここはViteの開発用サーバ(development server)の設定です。Webpackにもありましたが、Viteには開発用のサーバがあり、HMR (Hot Module Replacement)を提供してくれます。Ruby on Railsのjsbundling gemでesbuildを選択しただけの場合は、HMRのためのdevelopment serverはありませんので、HMRが使えません。この辺りはViteを使う際のメリットです(Rails ERBの開発がメインだとHMRのメリットをあまり感じないのですが、ブラウザステートが深くなると便利だなぁと私も感じます)。
でもVite development serverはメリットだけではありません。デメリットもあります。今回のセットアップは本番環境ではRuby on RailsとReactが同じホスト名、同じポートのサーバから提供されるのですが、development serverを使うと開発時はそうなりません。ポート番号が変わってしまうと、ブラウザのCORSなどのセキュリティ、cookieの読み込みなどに影響が出ます。
上記の設定はこの不便を解消するもので、development serverを使っていても、proxyを使ってあたかもポート番号が変更されていないように見せかけるものです。詳しくはGitHubのファイルのコメントを確認してください。
lib/tasks/react_router.rake
desc "Install npm packages for the React Router app"
task :npm_install do
Dir.chdir("#{Dir.pwd}/frontend-react-router") do
puts "Install npm packages ..."
system("npm", "install")
end
end
desc "Start React Router Dev Server"
task dev: [ :npm_install ] do
Dir.chdir("#{Dir.pwd}/frontend-react-router") do
system("npm", "run", "dev")
end
end
...
Rake::Task["assets:precompile"].enhance([ "react_router:build" ])
...
主なものだけを上記に掲載しています。詳しくはGitHub上のファイルを確認してください。
この辺りは細かい自動化のためのスクリプトです。bin/rails react_router:dev
を実行するとNPMパッケージが自動でインストールされ、development用サーバが立ちががります。
また最後のenhance
と記述されているところでは、build
タスクをbin/rails assets:precompile
の中に組み入れています。Ruby on RailsのCI/CDでは必ずbin/rails assets:precompile
が呼ばれますので、こうすることで自動的にReact Routerアプリのビルドも実行してくれます。つまりCI/CDのスクリプトをあまり変えずに済むようになります。これはjsbundling rails gemと同じやり方です。
CI/CDについてはDockerfile
を確認してください。
上記でRuby on RailsとReact Router v7 SPAモードを統合した環境が作れます。詳しくはGitHubのREADME.md
をご確認ください。
メリット
従来のReactとRuby on Railsの統合と比較した場合
- クライアントサイドのルータが最初から組み込まれています。React Routerを別途インストールする必要がありません。
- React RouterをFrameworkモードで使用すると、自動的なcode-splittingによる初期バンドルサイズの軽減をしてくれます。✂️
- React RouterをFrameworkモードで使用すると、loaderを使ってAPIからデータフェッチをするパターンになります。これは
useEffect
等を使ってデータフェッチする方法と比べて画面遷移をコントロールしやすく(「待ちUI」が作りやすいなど)、また画面のレンダリングを待たずにデータフェッチができますのでUXの改善に繋がります。
詳しくはReact公式ブログの"Limitations of Build Tools"を参照してください。
別のサーバにReact SPAの静的ファイルをホストした場合と比べて
例えばReactの静的ファイルをAWS S3からCDNにホストした場合や、静的ホスティングサービスを使った場合などです。
- 別のサーバに静的ファイルをホストした場合、(前にNGINXをおいたりして工夫しない限り)ホスト名が変わります。ホスト名が変わるとCORSの設定が必要になったり、cookieが共有できないなどの問題が発生します。🍪
- 特にcookieを共有できないとRailsの認証システムがそのまま使えなくなり、複雑になります。
フロントとバックのサーバを分けるメリットとしては、サーバの負荷の問題が言われることがあります。ただし最近ではCloudflareなどのCDNの設置も簡単になりましたし、最近ではThrusterというgemが37signalsから公開され、アプリサーバのPumaの前に設置して使えます。これはGolangで書かれた高速なhttp2サーバで、静的ファイルを処理してくれます。ThrusterとPumaを組み合わせるとNGINXを設置しなくても、Railsのサーバだけで高速・効率的に静的ファイルを処理できます。今回のDockerfile
でもThrusterを使っています(Cloudflare CDNも使っているので、二重感は否定できませんが)。
Next.jsのSSRサーバにフロントを乗せた場合と比較して
- Ruby on RailsとReactアプリを同時にデプロイできますので、Next.jsの場合と比べてデプロイがシンプルになります。
- Next.jsはVercelに支払うお金が心配ですが、一方でRailsの
public
からホストするSPAならお金のことを心配しないで済みます。💸 - Railsの
public
からホストするSPAなら、Cookieが共有できるようになりますので、認証が簡単になります。
モダンなReactを、Ruby on Railsで楽しもう
SSRフレームワークの流行に影響されて、SPAにもフレームワークの波が押し寄せてきました。Remix => React Routerの例が顕著ですが、SSRで研ぎ澄まされた技術がSPAにも適応されたという感覚があります。そしてReactチームも公式に「SPAフレームワークを使いましょう!」と言っています。
Ruby on Railsをバックエンドとしつつ、簡単にReactを使いたいという開発者も、この波に乗らないと、気づいたらレガシーReactアプリを開発してしまいます。
今回はほんの一例ですが、うまい具合にReact SPAフレームワークとRuby on Railsを統合して、快適な開発をしていきましょう。
そしてついでに、Reactも良いのですが、それよりさらに先進的(だと私は思っている)なHotwireにも挑戦しましょう!!
ビデオ
本記事やデモアプリの作成に取り掛かる前に、こういうビデオを作りました。今回のデモを検討する初期で作成したビデオですので、少し作り方が異なりますがご覧ください!
こっちは従来のReactとRuby on Railsの統合方法について紹介したビデオです。
Kamalの参考記事
Kamalを使ったデプロイについては、以前にZennの記事を書きましたので、参考にしてください。
Discussion
目から鱗です!勉強になりました。
ありがとうございます
ExpoはReact Nativeのメタフレームワークなので本記事とあんまり関係ないかもしれませんね。
React Nativeの界隈では有名です😊