BLUF (Bottom Line Up Front): Migrating a legacy Rails application from a disjointed jQuery/React setup to Hotwire should not be a “big bang” rewrite. The correct architectural approach is the Strangler Pattern: isolating complex frontend components and replacing them individually with Turbo Frames and Stimulus controllers, utilizing server-rendered DOM diffing.
Phase 1: The SPA Complexity Trap
Many legacy applications introduced single-page application (SPA) frameworks like React or Vue for specific features, creating a split brain. The server maintains state, but the client duplicates it, leading to complex API endpoints and state synchronization bugs.
Synthetic Engineering Context: The Legacy Component
Consider a legacy Vue.js component used just to update a user’s status inline without reloading the page.
// Legacy Vue component inside a Rails view
new Vue({
el: '#status-updater',
data: { status: 'active' },
methods: {
updateStatus(newStatus) {
fetch('/api/users/1/status', {
method: 'PATCH',
body: JSON.stringify({ status: newStatus })
}).then(() => this.status = newStatus);
}
}
});
This requires maintaining a separate API route, JSON serializers, and frontend build tooling just for a simple UI update.
Phase 2: The Hotwire Replacement
Hotwire eliminates the API requirement. We replace the JSON endpoint with standard HTML rendering and let Turbo handle the DOM injection.
Execution: Turbo Frames
Wrap the target area in a Turbo Frame. Any link or form submission inside this frame will be intercepted, and the response will only update this specific block of the DOM.
<%# app/views/users/_status.html.erb %>
<%= turbo_frame_tag "user_status_#{user.id}" do %>
<span class="status"><%= user.status %></span>
<%= button_to "Mark Inactive",
user_status_path(user, status: 'inactive'),
method: :patch %>
<% end %>
When the controller processes the update, it simply renders the same partial.
# app/controllers/user_statuses_controller.rb
class UserStatusesController < ApplicationController
def update
@user = User.find(params[:id])
@user.update(status: params[:status])
# Renders the partial. Turbo automatically extracts the matching frame
# and patches the DOM in the browser.
render partial: "users/status", locals: { user: @user }
end
end
By leveraging Stimulus controllers for minor JS behaviors (like toggling a modal) and Turbo Frames for state, we delete the Vue application entirely.
Phase 3: Next Steps & Risk Mitigation
The Hotwire migration simplifies the stack, but improperly sized Turbo Frames can lead to unnecessary database queries if you render too much HTML. You must ensure your controllers are highly optimized.
Need Help Stabilizing Your Legacy App? We help engineering teams delete thousands of lines of legacy JavaScript by shifting state management back to the server via Hotwire. Connect with USEO to plan your frontend modernization.