The go-to resource for upgrading Ruby, Rails, and your dependencies.

Bridging the Gap: Integrating Modern HMR into Old Rails Apps


In the early days of photography, subjects had to sit perfectly still for minutes at a time while the photographic plate was exposed. Any movement would result in a blur, ruining the image and requiring the entire process to start over. It was a deliberate process that demanded immense patience.

Similarly, developing frontend interfaces in older Ruby on Rails applications often feels like exposing an early photograph. You make a small change to a CSS class or a React component, save the file, and then wait for the entire page to reload. In doing so, you lose your scroll position, any data entered into complex forms, and the current state of your UI components. It is a workflow that demands patience, disrupts concentration, and ultimately costs engineering teams significant time in lost productivity.

Hot Module Replacement (HMR) is a feature of modern JavaScript development servers that allows modules to be updated in the browser at runtime without requiring a full page refresh. When a developer saves a file, the development server sends the updated module to the browser via WebSockets. The browser then swaps out the old module for the new one, preserving the current state of the application.

Before we get into that, though, let’s take a step back and examine the structural reality of frontend tooling in legacy Rails applications.

The Structural Reality of Legacy Frontend Tooling

Historically, Rails applications relied on Sprockets — the Asset Pipeline — to compile, minify, and serve JavaScript and CSS. Sprockets is reliable and straightforward to use. However, it was designed in an era before modern JavaScript frameworks and HMR existed. It fundamentally operates by concatenating files and requires a full page reload to reflect changes.

Later, Rails introduced Webpacker to bridge the gap with the NPM ecosystem. While Webpacker supported HMR, its complex configuration and slow compilation times frustrated developers. Today, Webpacker is officially retired, leaving many legacy applications in a precarious position regarding their frontend tooling.

Three Paths to Modernization

When engineering teams decide to modernize their frontend workflow, they typically face three major approaches. Depending on the particular circumstances of your application, one of them may be more useful than the other two.

Shakapacker (The Webpacker Successor)

Shakapacker is the community-maintained successor to Webpacker.

  • Pros: If your application is already deeply entangled with Webpacker, upgrading to Shakapacker is the path of least resistance. It supports HMR and maintains a similar configuration structure.
  • Cons: It still relies on Webpack under the hood, which can be memory-intensive and slower to compile than newer build tools.

jsbundling-rails (The Rails Default)

Introduced in Rails 7, jsbundling-rails provides a lightweight wrapper around modern bundlers like esbuild, rollup, or Webpack.

  • Pros: It aligns with the current Rails “omakase” philosophy, keeping the Ruby side minimal.
  • Cons: Strictly speaking, when using esbuild with jsbundling-rails, you typically get live reloading rather than true HMR. The browser still refreshes the page automatically when files change, which means application state is lost.

Vite Ruby

Vite Ruby integrates the Vite build tool into Rails applications. Generally speaking, this is my preferred method for greenfield and legacy apps alike.

  • Pros: Vite uses native ES modules during development, meaning it doesn’t need to bundle your entire application before starting the dev server. It provides true HMR for JavaScript, CSS, and frameworks like Vue, React, or Svelte.
  • Cons: It introduces a new tool to your stack and requires running a separate Vite development server alongside your Rails server.

A Practical Walkthrough: Integrating Vite Ruby

Let’s illustrate how to integrate Vite Ruby into an older Rails application. This walkthrough assumes you have a standard Rails application that currently relies on Sprockets or an outdated version of Webpacker.

Step 1: Installation

First, we add the vite_rails gem to our Gemfile:

$ bundle add vite_rails

Next, we run the installation generator. This command will scaffold the necessary configuration files and update your package.json.

$ bundle exec vite install

When you run this command, you will see output similar to the following:

Creating binstub
...snip...
Installing frontend dependencies
...snip...
Vite Ruby successfully installed!

We can verify that the installation succeeded by checking for the presence of the vite.json configuration file and the app/frontend directory. We can notice a few things here: Vite Ruby expects your modern frontend code to live in app/frontend by default, separating it cleanly from your traditional app/assets.

Step 2: Configuration and Entrypoints

Vite creates an entrypoint file at app/frontend/entrypoints/application.js.

Let’s put the following code into app/frontend/entrypoints/application.js to demonstrate HMR with a basic CSS import:

import './application.css'
console.log('Vite with HMR is active!')

And in app/frontend/entrypoints/application.css:

body {
  background-color: #f0f8ff; /* Alice Blue */
}

Step 3: Updating the Layout

To serve these files, we need to update our Rails layout. Open app/views/layouts/application.html.erb and add the Vite tag helpers.

<head>
  <!-- Keep your existing Sprockets tags if needed -->
  <%= stylesheet_link_tag 'application', media: 'all' %>
  
  <!-- Add the Vite tags -->
  <%= vite_client_tag %>
  <%= vite_javascript_tag 'application' %>
</head>

Note the use of <%= vite_client_tag %>. This specific tag is crucial — it injects the WebSocket client that listens for HMR updates from the Vite development server.

Step 4: Running the Servers

To benefit from HMR, we must run both the Rails server and the Vite development server. The vite install command creates a bin/dev script that uses Foreman — or similar tools — to run both processes simultaneously.

$ bin/dev

When you load your application in the browser, you will see the Alice Blue background. If you change the background color in application.css and save the file, the browser will update almost immediately — without a full page reload.

Addressing Complexity: Gradual Migration

One may wonder: if we integrate Vite, do we have to rewrite all our existing Sprockets assets immediately?

The answer is straightforward: No.

You can run Vite Ruby alongside the traditional Asset Pipeline. This is a significant advantage for large legacy applications. Teams can adopt Vite for new features or gradually migrate existing components, reducing the risk associated with a complete rewrite. You can keep your legacy jQuery code in Sprockets while building new interactive forms with React and Vite.

Warnings and Caveats

Of course, introducing a new build system is not without risks.

Before you commit to Vite Ruby, be aware that you are introducing another moving part to your deployment process. In production, Vite compiles your assets into static files, much like Webpacker did. You must ensure your CI/CD pipeline is updated to run the Vite compilation step — typically bundle exec rails assets:precompile, which Vite hooks into.

Additionally, if your application runs in a highly constrained Docker environment for development, configuring the HMR WebSocket connection can occasionally require manual port mapping adjustments. The Vite server typically runs on port 3036, and the browser must be able to reach that port directly.

Conclusion

Integrating Hot Module Replacement into a legacy Rails application represents a fundamental shift in developer experience. By adopting tools like Vite Ruby, engineering teams can escape the slow cycle of full-page reloads, preserving application state and maintaining their flow.

While the transition requires careful configuration and an understanding of the underlying build processes, the compounding dividends in developer productivity make it a highly pragmatic investment for the long-term sustainability of the application.

Sponsored by Durable Programming

Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.

Hire Durable Programming