Rails 8 Hotwire: The Modern SPA

By kirt@optimalcadence.com, December 19, 2024

As programming technologies trend toward excessive complexity in the name of improving user experience, Rails 8 dares to be different. The challenges of building modern single-page applications (SPAs) with tools like React, Vue, and Svelte have given rise to even more intricate solutions, such as Next.js and Nuxt. These tools are remarkable, no doubt—but is it time for a new evolutionary step forward?

As application complexity increases, we often lose sight of the original motivations behind our choice of tools. It’s a cycle I’ve observed time and time again since I started programming professionally in 1999. (For a deeper dive, check out my earlier post, Ruby on Rails 8: The SPA Killer—I’ll spare you the full rant here.)

The Promise and Pitfalls of SPAs

The original appeal of SPA architecture was to provide a more responsive and reactive user experience. Gone are the days when clicking a link or button resulted in staring at a blank page while the browser worked to fetch the next set of content. Properly written SPAs ensure the interface reacts instantly, with data exchanges happening seamlessly in the background.

However, this programming paradigm brought its own set of challenges. Building SPAs often means creating two separate applications: a front-end app and a back-end API. This approach introduces unnecessary complexity by duplicating efforts such as state management, routing, models, and business logic.

On top of that, SPAs frequently encounter a "data mismatch" problem—the data needed by the client rarely aligns perfectly with what the API delivers. It’s not uncommon for a single UI use case to require multiple API calls to fetch all the necessary data.

Wait... I promised not to rant.

Rails 8 and the SPA

In this post, I’ll demonstrate the stark contrast between how traditional SPAs (built with tools like React, Vue, or Svelte) operate versus how Rails 8, leveraging Hotwire, achieves the same responsive and reactive results—with far less complexity.

Let’s start with a layout you’ll recognize from many web applications: a header, a sidebar, and a main panel where most user interactions occur.



Traditional SPA Applications: A Complex Web

In a traditional SPA application, the layout is built from a hierarchy of components organized to create a three-panel design. While implementation details can vary, a typical breakdown of components and sub-components might look something like this:
  • Header
    • HamburgerMenu
    • AvatarMenu
  • SideBar
    • SideBarLink
  • MainPanel
    • WidgetChartCard
    • WidgetAnalyticsCard
    • WidgetTableCard
    • AddButton
    • WidgetTable
    • ActionButton
    • PaginationControls
To be considered reactive, an application must respond immediately to user actions—whether that’s clicking a button, using a keyboard shortcut, or entering data into a form field—without requiring a full page reload. This responsiveness is often achieved by calling an API endpoint in the background to read, create, update, or delete data stored in a database.

For example, when a user clicks a "delete row" icon in a table, the app sends a background request to delete the corresponding data. Once the server confirms the deletion, the row is removed from the UI, and a success message appears. Seamless, right?

The SPA Approach: How It Works

In SPA architecture, every component is generated using JavaScript (or TypeScript) and rendered as HTML in the browser. This setup gives developers programmatic control over the entire component tree, allowing them to dynamically manipulate the UI. Ideally, components update automatically when their underlying data changes. For instance, if a list of widgets is updated by the server, the table component will react by removing the deleted widget from view.

Sounds simple? Not quite.

Getting SPAs to function correctly requires a significant amount of specialized skill. This complexity is a primary reason for the rise of specialization in front-end and back-end development—a shift that didn’t always exist in earlier programming paradigms.

Enter the "One-Person Framework"

Rails 8 takes a radically different approach, leveraging its built-in templating system to achieve the same reactive, responsive experience as SPAs—but without the added complexity. Instead of generating and rendering HTML in the browser, Rails renders it on the server and sends it to the browser as needed.

This isn’t a new concept, but Rails gives it a modern twist with Hotwire.

Like traditional SPAs, Hotwire can request updates in the background. However, instead of sending raw data that the client must process and render, the server sends pre-rendered HTML snippets. Only the necessary parts of the page are updated, leaving the rest of the HTML intact. The result? A reactive, responsive experience with much less overhead.

Why Is This Better?
I’m glad you asked! Here are some key advantages of this approach:
  1. Centralized State Management: Most of the application state resides on the server, simplifying maintenance.
  2. Unified Routing: The server handles all routing decisions, eliminating the split responsibilities between front-end and back-end routing.
  3. Simplified Security: With fewer moving parts, security can be managed more effectively on the server.
  4. Streamlined Data Transformation: The server can gather and transform all the data needed to render the UI, avoiding the "data mismatch" issues common in SPAs.
  5. Simpler Architecture: Less code—a LOT less code—means fewer opportunities for bugs and easier maintenance.
  6. Reduced Specialization: Rails 8 significantly lowers the need for separate front-end and back-end specialists, aligning perfectly with the philosophy of the "One-Person Framework."

How Does It Work?
Rails’ built-in templating framework allows developers to divide the UI into partials—small, reusable snippets of HTML. Partials function much like components in SPAs, representing cohesive sections of the overall interface.

For example, in the application layout mentioned above, partials might correspond to the following UI sections:



You’ll notice I’ve added another UI element: a modal dialog box for adding or editing widgets.

Like components in SPA frameworks, there are no strict rules for how granular your partials should be. The example here is just one way to divide the interface. If you ever find that splitting a partial into smaller parts makes sense, it’s straightforward to do so.

The advantage of breaking the page into partials is that each one represents a discrete section of the UI that can be updated independently with incoming HTML. This means you don’t need to replace the entire page when only a small portion of it needs to change.

A Use Case: Deleting a Row

Let’s walk through a simple use case: deleting a row from the table. How would this process play out?

Without Hotwire
  1. The user clicks the "X" icon to delete a row.
  2. A new URL is requested.
  3. The page goes blank while the browser spins, contacting the server.
  4. The entire page (minus the deleted row) is returned and re-rendered in the browser.
Blech!

With Hotwire
Hotwire provides several ways to handle this more elegantly:
  • Turbo Frames
  • Turbo Streams
  • Turbo Morphs

Each method has its strengths, but they all share a common goal: updating only the necessary parts of the page without reloading the entire thing.

Turbo Frames

A Turbo Frame in Hotwire represents a specific portion of the page. While it’s conceptually similar to a partial, its purpose is to segment a section of the UI where independent updates can occur. Events within a Turbo Frame affect only the HTML inside that frame.

In our example, the table displaying widgets could be wrapped in a Turbo Frame. When the user clicks the delete icon, only the contents of the frame—the table—are updated to reflect the deletion, leaving the rest of the page untouched.

This approach not only improves responsiveness but also simplifies the developer’s job by reducing the need for complex JavaScript logic to manage UI updates.


Now, let’s see Turbo Frames in action. When a user interacts with an element inside a frame—say, clicking the delete icon on a row—Hotwire quietly handles the process in the background.

Here’s what happens:
  1. Hotwire sends a request to the server to process the delete action.
  2. The server removes the record from the database and generates updated HTML for the affected frame (in this case, the table).
  3. The browser receives the new HTML and Hotwire swaps it into place, removing the deleted row without refreshing the entire page.

Simple and seamless!

Turbo Frames are fantastic for scenarios like this, where updates are localized to a specific part of the page. However, what if deleting that row affects other parts of the page? For instance, what if the chart showing widget statistics needs to update too?



As shown in the diagram above, the row is removed from the table, but unless the chart is part of the same Turbo Frame, it won’t be updated.

So, why not include the chart in the frame?
While that’s possible, the larger the frame’s scope, the more apparent the page change becomes to the user. Programmers must carefully decide how to use Turbo Frames and determine the right level of granularity for optimal user experience.

But wait—there’s an even more flexible solution!

Turbo Streams

Turbo Streams are like Turbo Frames with superpowers. They allow you to mutate any part of the DOM with a variety of actions, such as replace, append, prepend, or remove. Let’s revisit our delete example to see how this works:
  1. The user clicks the delete icon on a table row.
  2. Hotwire sends a delete request to the server.
  3. The server processes the request, deletes the row, and returns a response with two actions:
    • Remove the table row with the specified identifier.
    • Update the chart with new data.
  4. Hotwire parses the response and executes the actions.

With Turbo Streams, you can achieve a responsive and interactive user experience without overloading the server or the browser. Unlike traditional server-side rendering, the back end only needs to load the relevant data and generate the necessary HTML to meet the user’s goal.

Turbo Drive Morph

Rails 8 introduces Turbo Drive Morph, a new way to create responsive and reactive applications. While similar to the traditional server-side rendering approach, it has two key differences:
  1. When the user clicks the delete icon, Hotwire sends a background request to the server to delete the row.
  2. Instead of refreshing the entire page, Hotwire compares the returned HTML with the current page and seamlessly updates only the elements that have changed.

This morphing process makes page updates feel seamless and fluid to the user. Adapting existing applications to use Turbo Morph is straightforward, requiring minimal configuration changes while preserving much of the existing flow.

Summary

I hope this article has given you a glimpse into how Rails 8 enables developers to build reactive and responsive applications without the complexity of managing separate front-end and back-end architectures.

Personally, I’m sold. After over a decade of building traditional SPAs, transitioning to Hotwire has been a game-changer. My productivity has skyrocketed, and I genuinely enjoy programming more than ever.