Boring Rails

Thinking in Hotwire: Progressive Enhancement

Aug 16th, 2022 8 min read
This post is part of Hotwire Summer: a new season of content on Boring Rails!

There are many tutorials about how to get started with Hotwire and how to use the individual pieces. But one thing that took me a while to grasp was how to “think in Hotwire”.

Hotwire itself is an overarching concept (HTML-over-the-wire) and you’ll need to know when to use the different pieces (Turbo Drive, Frames, Streams, Stimulus, Turbo Native, Strada).

Because Hotwire is a collection of tools, you can solve problems multiple ways. There are features you can build with Frames that you could also build with Streams. You can always drop to a “lower level” tool and make something work.

So how should you know when to reach for each tool?

I think the best approach comes from the earliest days of web development: progressive enhancement.

Scaffolds and CRUD without Hotwire

Let’s start with the absolute bare bones: the pre-Hotwire days of Rails. You would scaffold out a resource-based controller.

To add a comment to a post, you would have a comments_controller with a new action to render a form.

You would submit a new comment to comments#create where the comment model would be saved and then we send a redirect back to the post#show route.

It’s as boring as you can get, but it works and is the foundation upon which RESTful web applications were built.

Adding Turbo Drive

So what if we wanted to enhance the experience just the tiniest amount? We could use the first layer of Hotwire: Turbo Drive. If you’re on a recent version of Rails, Turbo Drive is on by default, even if you didn’t realize it.

Now when you click + Add, instead of a full-page reload of comments#new, we use Turbo Drive to fetch the page via AJAX and then Turbo Drive swaps out the <body> contents. This behavior (originally called Turbolinks) mimics the responsiveness of a single-page application, but everything is still server-rendered in Rails.

Turbo Drive does the same thing when you submit the comment form: instead of a full page reload, the <body> is swapped, even when we do a redirect. The JS and Rails portions of Turbo Drive work together to make this seamless (…assuming you follow the right conventions!).

So you could stop right here and technically you are using Hotwire. Turbo Drive is mostly invisible for Rails developers. If you don’t need any more interactivity, you don’t need to go any deeper.

Adding Turbo Frames

While Turbo Drive originated as Turbolinks, Turbo Frames are a new concept in Hotwire. I’ve described Turbo Frames before as “iFrames but they work how you would want them to”. In Hotwire, Turbo Frames are used for partial page updates.

A Frame works similar to Turbo Drive: navigation visits and form submissions are intercepted, made via AJAX, and then the response is swapped into the page. But with Frames, instead of swapping the whole <body> tag, only the contents of a matching Turbo Frame is swapped.

So if you have a link inside a Turbo Frame with an id of comment_123, Hotwire will look for a matching <turbo-frame id='comment_123'> tag in the response to swap.

The canonical use-case is inline editing. If you have a list of comments and you wrap a frame around each comment, you can add a edit button. Clicking the edit button can render a comments#edit response, but swap out the form with the frame instead of loading a separate page.

Adding Turbo Streams

Turbo Drive replaces the whole <body> tag. Turbo Frames replace a frame. Turbo Streams go even more granular.

Turbo Streams provide a set of standard operations for manipulating the HTML on the page. You can add, remove, or replace content.

In our comment example, if you want to delete a comment, you can respond to the request with a Turbo Stream to remove the comment from the page. Or if you create a comment, you can add it to the end of the list.

Turbo Streams are the Hotwire version of another classic Rails technique: server-generated JavaScript response (SJR). In the past, you could return back a snippet of JavaScript that would get executed in the page. But this caused issues with Content Security Policies and was a bit too permissive.

Turbo Streams provides a CRUD-like abstraction to handle nearly anything you should have been doing with SJR.

Adding Turbo Streams + Action Cable

Turbo Stream responses can also be broadcast out-of-band using Action Cable. One major point of confusion is that Turbo Streams do not have to be sent over Action Cable or web sockets, you can use them within a normal HTTP request/response cycle.

But let’s say we want real-time functionality so that when someone else adds a comment, it gets added to our view of the page without doing a refresh.

Adding Stimulus

Hotwire guides you to write as much of your application using server rendered Ruby code as possible. But there are cases where you still want extra sprinkles of JavaScript functionality on the client side.

Stimulus is the recommended JavaScript framework for attaching functions to your HTML markup and responding to simple browser events.

One thing to note is that Stimulus does not provide anything to support client side rendering. There are no templates or JSX. If you find yourself generating a lot of HTML in your Stimulus controllers, you should take a step back and re-assess.

Stimulus controllers are often general-purpose and under 50 lines of code.

If you want to support collapsing comments, you could add a simple Stimulus controller to show or expand a comment. (Note: in this specific case, a Hotwire enthusiast might reach for native HTML elements like <detail> but I digress…)

Adding custom components / React / client side JavaScript

There is a time and place for tools like React for high interactivity components. Whether you use a tool like React, Vue or Web Components, there are things that are best built on the client side that go beyond what Stimulus can handle.

For this, you can add a small, isolated component to the page. An example from Rails is the Trix rich text editor: it is a standard <trix-editor> web component.

In our comment example, maybe you want to add an emoji picker or some kind of fancy drag and drop file uploader. You might wrap these richer JavaScript libraries in a Stimulus controller to help manage Turbo lifecycle events.

Hotwire advocates don’t say that you should never write JavaScript components, but rather that you should start with the other tools and only reach for this level when you really need it.

Adding Turbo Native

Once you’ve built a feature in your web application, you can use the Turbo Native iOS and Android adapters to create mobile applications that wrap display your same app content inside of a web view.

This is not a “mobile optimized web app” but a real Swift or Kotlin app that renders pages from your Rails app.

There are some conveniences to create native action bars and buttons.

The main idea is to re-use as much of your Rails views as possible and then Turbo Native provides a mechanism to eject and write some screens in the platform native languages. Again, it’s the idea of progressive enhancement.

The flagship example app (the Hey email client) for instance has a fully native inbox screen, but many of the secondary or account setting screens are wrappers around Rails views.

The Turbo Native functionality should still be considered as a beta release. Most developers writing Hotwire applications are not using the native features, but it is nice to know that some folks are laying the groundwork for this style of development.

Adding Strada

The last piece of Hotwire is the [as of summer 2022] unreleased Strada library. This is an extension of the Turbo Native functionality and helps bridge the gap when you need to communicate between the Swift or Kotlin parts of your application and the HTML/Rails portions.

This library will be a convenience extension to Turbo Native, it doesn’t add anything fundamentally new to the mix.

Until the project is actually released, it’s fine to ignore.

Wrap it up

Hotwire builds on the idea of progressive enhancement. You should use the least amount of the tooling as possible to achieve your desired outcome. As you move “down the stack” of what Hotwire offers, you trade off more power for more complexity.

The nice part about this approach is that you can build versions of features quickly, test and iterate based on feedback, and then layer on more real-time and interactive functionality as needed.

The progressive enhancement approach pairs beautifully with HTML and Hotwire encourages you to explore what can be done with native browser elements and only layers on extra tooling to complement or upgrade the platform.

Hotwire as a brand is an umbrella for several different tools and hopefully this overview will help you know how far down to reach and how the individual tools come together to build a cohesive and compelling stack.

Was this article valuable? Subscribe to the low-volume, high-signal newsletter. No spam. All killer, no filler.