RailsとReact Routerをシンプルに繋げる
モチベーション
Ruby on Railsは大好きだが、Hotwire には慣れない。モダンな方法で画面を実装したい。
方針
-
React Router の SPA用設定 (
ssr: false
)を使う- 個人開発のシンプルなアプリにとってSSRは不要な複雑さがある
- kamal でデプロイする。ワンサーバーでRailsのコンテナだけ動かす
- react-router <-> Rails間はクッキーでセッションを貼る
方法
react-routerのセットアップ
# railsルートでfrontend ディレクトリにreact-router appを作成する
npx create-react-router@latest frontend
ssr設定をfalseに。 loader
action
などは使えなくなるので、 clientLoader
clientAction
を使うようにすること。
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
- ssr: true,
+ ssr: false,
} satisfies Config;
viteの設定。HMRのためのサーバのポートを適当にずらしてあげる必要がある
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
server: {
// only for development
hmr: {
port: 3001, // 開発環境のrailsのプロキシを迂回するようにreact-router dev serverのデフォルトポート(5173)から明示的にずらす
},
},
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
});
Railsを設定
gem "rack-proxy", "~> 0.7.7"
開発用に使うRackミドルウェア rack-proxy をインストールしておく。
次にルーティングを設定する。これが設定の肝
+ class FrontendStatic < ActionDispatch::Static; end
+ config.middleware.insert_before ActionDispatch::Static, FrontendStatic, "#{Rails.root}/public/react-router/client"
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
# ... ここに好きなルーティングを書く ...
# SPA用の設定
# ---以下の設定はroutingの最後にマッチするようにファイルの最後に置いてください---
if Rails.env.production? || ENV["FORCE_SPA"]
class SPAHandler
def initialize
@file_handler = ActionDispatch::FileHandler.new(Rails.root.join("public", "react-router", "client").to_s)
end
def call(env)
env["PATH_INFO"] = "index.html" # always serve the index.html
@file_handler.call(env)
end
end
get "*path", to: SPAHandler.new
else
proxy = Rack::Proxy.new(backend: "http://localhost:5173") # to react-router dev server
root to: proxy
get "*path", to: proxy
end
end
本番環境ではActionDispatch::Staticミドルウェアを使い、優先してreact-routerのビルド物を返すようにする。これにより.js, .cssなどのアセットが配信される。
またSPAHandler ミドルウェアにより、Rails上でマッチしなかったルーティングはすべて react-routerのビルド結果の index.html を返すようになる。
開発環境では rack-proxyを使い、Rails上でマッチしなかったルーティングをすべて react-routerのdev serverに流す。 rootも設定しておかないとwelcome画面が出るのでrootも設定する。
ビルドの設定
Railsのassets:precompileをフックして react-routerのビルドを行うようにする。
lib/tasks/react_router.rake を作成して以下のようにしておく。
namespace :react_router do
desc "Build React Router App"
task build: :environment do |_, args|
Dir.chdir(Rails.root.join("frontend")) do
system("pnpm", "run", "build", exception: true)
# Copy the react-router build to the Rails public directory
FileUtils.cp_r("build", Rails.root.join("public", "react-router"))
end
end
task clobber: :environment do
FileUtils.rm_rf(Rails.root.join("public", "react-router"))
end
end
Rake::Task["assets:precompile"].enhance([ "react_router:build" ])
Rake::Task["assets:clobber"].enhance([ "react_router:clobber" ])
ビルド生成物を .gitignoreに追加しておく
+ /public/react-router
Procfileを作成(optional)
開発に必要なサーバを bin/dev
で一括で起動して開発できるようにする。
Rails のプロジェクトに bin/dev が存在していなかったら以下を作る。 [1]
#!/usr/bin/env sh
if ! gem list foreman -i --silent; then
echo "Installing foreman..."
gem install foreman
fi
# Default to port 3000 if not specified
export PORT="${PORT:-3000}"
# Let the debug gem allow remote connections,
# but avoid loading until `debugger` is called
export RUBY_DEBUG_OPEN="true"
export RUBY_DEBUG_LAZY="true"
exec foreman start -f Procfile.dev "$@"
web: bin/rails server
frontend: cd frontend && pnpm run dev
まとめ
以上でデプロイ可能なRailsモダンフロントエンドができる。Railsやっていきましょう
Discussion