Progressive Migration from an Existing Rails App
You do not need to rewrite your entire application to use ruact. This guide shows how to add ruact to an existing Rails app and migrate one page at a time — while all existing pages continue to work.
Strategy overview
Section titled “Strategy overview”ruact is opt-in per action. A controller action that does not call ruact_render is unaffected. This means you can:
- Add the gem and the controller concern without touching any existing views
- Pick one new feature or page to build with ruact
- Gradually migrate other pages when it makes sense
Existing HTML responses, Turbo, Hotwire, and JSON APIs all coexist without conflict.
Step 1: Add the gem
Section titled “Step 1: Add the gem”Add to Gemfile:
gem "ruact"bundle installStep 2: Run the install generator
Section titled “Step 2: Run the install generator”rails generate ruact:installThis creates app/javascript/components/, vite.config.js, and config/initializers/ruact.rb. ruact’s bootstrap entry and Flight runtime are hidden plumbing (a virtual module served from the gem), so nothing is written into app/javascript/ except your own components. It does not modify any existing views or controllers.
npm installStep 3: Add the concern to ApplicationController
Section titled “Step 3: Add the concern to ApplicationController”class ApplicationController < ActionController::Base include Ruact::Controller # ... your existing before_actions, helpers, etc.endThe concern does nothing until a controller action calls ruact_render. All existing render, redirect_to, and respond_to calls are unaffected.
Step 4: Migrate one action
Section titled “Step 4: Migrate one action”Choose one action to convert. Change render to ruact_render:
# Beforedef dashboard @stats = Stats.current render :dashboardend
# Afterdef dashboard @stats = Stats.current ruact_render # serves Flight payload or HTML shell depending on request typeendYour existing app/views/dashboard/dashboard.html.erb continues to work. Rails ERB rendering is unchanged — ruact wraps the output in a Flight payload.
Step 5: Update the layout for RSC pages
Section titled “Step 5: Update the layout for RSC pages”For pages that use ruact, the HTML layout needs to include the RSC client JavaScript. The install generator creates a dedicated layout or adds to your application layout:
<!DOCTYPE html><html> <head> <title>My App</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= vite_client_tag %> <%= vite_javascript_tag "application" %> </head> <body> <%= yield %> </body></html>You can use a separate layout for RSC actions and keep the default layout for legacy pages:
class DashboardController < ApplicationController layout "rsc"
def index ruact_render endendStep 6: Add client components incrementally
Section titled “Step 6: Add client components incrementally”As you migrate pages, extract interactive parts into "use client" components:
app/javascript/components/├── SearchInput.jsx ← "use client" — replaced a Stimulus controller├── ChartWidget.jsx ← "use client" — was a Turbo Frame└── DropdownMenu.jsx ← "use client" — replaced Alpine.jsYou do not need to migrate all interactivity at once. Stimulus controllers and Turbo Frames continue to work on non-RSC pages.
Mixing RSC and non-RSC pages
Section titled “Mixing RSC and non-RSC pages”It is perfectly valid to have some pages using ruact and others using traditional Rails HTML:
class PostsController < ApplicationController # RSC page def index @posts = Post.all ruact_render endend
class AdminController < ApplicationController # Traditional Rails HTML — unaffected def dashboard @stats = Stats.current render :dashboard endendThe RSC client only intercepts navigation to pages that respond to text/x-component. Navigation to traditional HTML pages causes a full page load.
Common migration patterns
Section titled “Common migration patterns”Replacing a Turbo Frame with a server component
Section titled “Replacing a Turbo Frame with a server component”Before (Turbo Frame):
<turbo-frame id="comments"> <%= render @comments %></turbo-frame>After (ruact):
<CommentsList comments={@comments} />The difference: with Turbo Frames, the server still renders HTML. With ruact, CommentsList is a client component that receives structured data and renders it in React.
Replacing a Stimulus controller with a client component
Section titled “Replacing a Stimulus controller with a client component”Before (Stimulus):
<div data-controller="counter" data-counter-initial-value="<%= @count %>"> <span data-counter-target="display"></span> <button data-action="click->counter#increment">+</button></div>After (ruact):
"use client"
export function Counter({ initialValue }) { const [count, setCount] = useState(initialValue) return ( <div> <span>{count}</span> <button onClick={() => setCount(count + 1)}>+</button> </div> )}<Counter initialValue={@count} />Verifying the migration
Section titled “Verifying the migration”Run rails ruact:doctor to check your setup:
rails ruact:doctorThe doctor verifies: manifest file exists, Vite is configured, client components are registered, and the Rails integration is correctly configured. See the ruact:doctor reference for details.