Hotwire

By AKM
January 10, 2025
With Rails 7 (late 2021), there was a bold new set of features launched called Hotwire or HTML over the wire. After developing multiple applications using it over the past couple of years, this is an introduction to the topic for those interested in getting into it. Some familiarity with basic web development technologies and Ruby on Rails framework is assumed. 

Introduction

For past several years, web applications have taken the approach of developing the client side (the HTML pages along with Javascript) in one of the Javascript frameworks like React. When you are using React, the server side implements a JSON API which the client side then calls to fetch and store data. This approach allows creating faster loading pages and single page applications. However, it also has its downsides. The major one being that the business logic of the application is divided between the server side and the client side. That makes the application harder to develop and modify when business requirements change. The older HTML-only web applications, albeit clunky, were easier to maintain.

To counter this approach and bring back all major business logic into the server side, Rails developed Hotwire. It is not a single technology but a collection of libraries that enables a certain style of development. All major application code is written on the server side and hence are in Ruby. But it still has most of the functionalities like Single-page applications, reusable UI components etc. that a Javascript framework provides. There is no JSON API and instead browser receives just plain old HTML, just like it used to. Hence the name, HTML (in contrast to than JSON) over the wire.

Let see each of the libraries which are part of Hotwire in some details.

Turbo drive

Turbo is a Javascript library that is part of Rails. With Turbo drive, all applications become single page applications (SPA). As a developer, you continue developing your application page-by-page, blissfully unaware of the SPA. On the client side, Turbo drive creates a persistent process. All page changes happen within that. Turbo changes all link clicks and all form submits into background requests (using fetch), gets the response from the server side, changes the browser's URL using History API and renders the HTML response on the browser. While rendering the response, Turbo replaces the HTML's <body> element and merges the contents of <head> element. The Javascript window and document objects persist from the previous to the new page.

👉 In any Rails 7+ application, Turbo drive is turned on by default. Which means all pages in the application are part of the SPA by default. In case needed, you can switch it off for particular pages. Or, you can decide to turn it off by default and only turn it on certain pages. To turn off Turbo drive from a particular link or a form, add data-turbo="false" to it. Adding it to any HTML element disables Turbo for all links and forms within it. Any links or forms where turbo is disabled, will be handled normally by the browser i.e. when the link is clicked, SPA will be stopped and the whole page will get rendered.

If you would like request to also trigger a full page render, regardless of whether that link had data-turbo="false" or not, then include the following meta tag in the page header:

<meta name="turbo-visit-control" content="reload">

To turn off Turbo drive across the whole application, in application.js:

Turbo.session.drive = false;

👉 Adding data attribute data-turbo-confirm on links will call the Javascript confirm method before the link is followed. Data attribute data-turbo-method specifies the HTTP method to use in the request (default is GET).

<a href="/comments/1234" data-turbo-method="delete" data-turbo-confirm="Do you want to delete this comment?">Delete this comment</a>

👉 By default, the whole application is part of the single SPA. However, you may want to limit the SPA to a certain URL, say all pages under /client should be part of one SPA but anything under /admin should be part of a different SPA. Then include the following in the page header:

<head>
  ...
  <meta name="turbo-root" content="/client">
</head>

This means that any time user is navigating from this page to another page under /client, only then Turbo will use SPA. Otherwise a full page render will get triggered.

👉 Turbo also prefetches links to speed up navigation. Any time a user hovers on a link for 100ms or more, Turbo will prefetch the page at that link. To disable this behaviour, add to the page header:

<meta name="turbo-prefetch" content="false">

Or disable it on any particular link by adding data-turbo-prefetch="false" on that link.

👉 When Turbo is swapping the old page by new page, by default it swaps the content and then resets the scroll position to the top. This mimics the default behaviour of the browsers on a regular page load. However, you can configure Turbo to morph, instead of swap, into the new page. This gives a smoother feel to the transition. Similarly, scroll position can be preserved if needed.

<head>
  ...
  <meta name="turbo-refresh-method" content="morph">
  <meta name="turbo-refresh-scroll" content="preserve">
</head>

Turbo frames

Imagine the home page of a newspaper website. Such a page typically has several sections - a navigation menu at the very top, a carousel with breaking news, list of recently published news stories and perhaps a sidebar with the most popular stories of the week. The server side application builds the page section by section and then sends over the whole page to the browser. Even though each section is independent of the others, all of them have to be built sequentially as part of the page.

To improve the page performance in such cases, you can wrap each such section into a <turbo-frame> HTML element. That gives you two things:

👉 Any link click or form submit inside a turbo-frame is scoped within that frame and the rest of the page remains as it is. Internally, Turbo will make a request, fetch the response, extract that frame from the response and only replace the contents of the requesting <turbo-frame> element with the contents of a matching <turbo-frame> element in the response. Note that the <turbo-frame> element itself remains unchanged and only its contents get swapped.

👉 Secondly, it is possible to lazy load that frame by providing a src attribute. Whenever the whole page loads, that frame will be initially empty. It will be loaded by Turbo by fetching the frame contents from the URL in src. That makes the page load faster because multiple frames can be loaded in parallel by the browser. Additionally, if the frame has a loading="lazy" attribute, it will not be loaded until it becomes visible.

If you would like to use a turbo-frame only for lazy loading and not for scoping navigation, you can ask Turbo to reload the page in the normal fashion on link clicks and form submits by setting the target to _top

<turbo-frame id="sidebar" src="/news/most-popular" target="_top">
    ...
</turbo-frame>

Note that It is also possible to set the target to another frame in which case that other frame's content will be updated from the response. 

If you want just one link or a form within the turbo-frame to render the full page, set the target just on that link:

<turbo-frame id="comment">
    <a href="/profile/edit" data-turbo-frame="_top">
      Edit profile
    </a>
</turbo-frame>

In case, you want to force a full page render from server side, regardless of whether the request came from within a turbo frame or not, include this header in the response:

<head>
  ...
  <meta name="turbo-visit-control" content="reload">
</head>

Although <turbo-frame> is somewhat inspired by iframes, they are very much part of the page's HTML DOM. They are styled by same CSS and are part of the same Javascript context. You may see other advantages of breaking down a page into frames. It helps with caching strategy later since now you simply cache each frame for whatever time is suitable for it. 

Turbo stream

As we saw above, turbo frames allow you to update just one frame on the page as a result of any link click or form submit. When you use Turbo frames, the server side application is blissfully unaware that the response it is sending is meant for a frame and not the whole page. Sometimes, that is what you want.

However, in case, you need to update multiple elements of the on a page with a single response, you have to use Turbo stream. In this case, the server side has to explicitly send a turbo stream response, instead of the usual HTML response. Each section in the turbo stream response should contain a target element on the page, the HTML snippet and how that element should be updated (called an action). There are 9 actions supported: append, prepend, replace, update, remove, before, after, morph, and refresh. 

In the Rails controller, it is possible to respond to any form submit with a turbo stream. Note that forms with method GET and links cannot have a turbo stream response.

respond_to do |format|
  format.turbo_stream { render :new_comment }
  format.html
end

Then in the new_comment.turbo_stream.erb view

<%= turbo_stream.append("comments-list", partial: "new_comment_form") %>
<%= turbo_stream.remove("add-comment-button") %>


Note that, replace replaces the element as well, while update leaves the element as it is (along with any Javascript handlers bound to it) and only replaces the innerHTML content. refresh means the whole page should be re-fetched.

Rails converts the above turbo stream ERB view into a response containing <turbo-stream> element. The Turbo Javascript library, running on the browser, updates the page based on this response.

Turbo stream is the spiritual successor of RJS and UJS that was present in older versions of Rails. However, a Turbo stream response cannot contain any Javascript, only HTML. With the supported 9 actions, usually Javascript is not required. But if it is needed, it will have to be handled using Stimulus (described below). 

It is possible to insert turbo-stream element into a regular HTML page. Turbo will then process the turbo-stream element after the page has been rendered. This allows you to modify the page immediately after it has finished loading.

Action Cable

WebSocket is a TCP protocol that is compatible with HTTP but supports 2-way communication in contrast to 1-way supported by HTTP. So, its possible to proactively send updates from the server to a client (browser) without the client initiating a request. So, this removes the need to periodically update a page every few seconds. WebSockets make it easy to implement live chat, notifications etc. Rails' Action Cable provides built in support for WebSockets. Action Cable uses the Pub-Sub pattern to manage the communication between server and clients.

Let us say, whenever a fresh news story is published, you would like that news story to appear for everyone that has the home page open at that time. In traditional HTTP, this is implemented by calling some Javascript every few seconds which checks and reloads the top news section on the home page. With WebSockets, you can instead have the server send an Action cable update to all open browsers whenever a fresh news story is published. This is how it works:

👉 Clients (i.e. the Javascipt or the frontend part of the app) should subscribe and establish a channel with the server. Server can then publish an update to all the subscribed clients any time. This is called Pub-Sub. So, there are two parts to the whole set up - server side and client side. Lets look at each of them.

👉 Firstly, Client establishes a connection on a URL (by default Rails configures it as /cable). Most commonly, client is identified using user_id

createConsumer('wss://example.com/cable') // most commonly done in a Javascript file which runs right at page initialisation

👉 Next, client subscribes to one or more channels using Javascript

// can be done as a result of some user action or by default for all users on page load
consumer.subscriptions.create({ channel: "NotificationChannel", content_type: "news" })

👉 consumer.subscriptions.create will send a request which will be handled by NotificationChannel controller's subscribed action on the server side. So, in this method, you can subscribe the client to a broadcasting channel (which is just a unique string identifier)

# app/channels/notification_channel.rb
class NotificationChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notif_#{params[:content_type]}" # subscribed to broadcast notif_news
  end

  def receive(data)
    # called if the client send some data
    # useful for a chat channel
  end
end

Then elsewhere in your code, whenever a fresh news story is published, send a broadcast to the correct channel

ActionCable.server.broadcast("notif_news", { title: "Headline123", link: "", body: "news in details" })

Rails make the subscription to a channel and broadcasting little easier if the channel corresponds to a a model i.e. ActiveRecord object in your app

def subscribed
  @article = Article.find(params[:id])
  stream_from @article # channel name is automatically generated
end

Then broadcasting to this channel can be done simply as:

NotificationChannel.broadcast_to(@article, @comment) # broadcasts to channel build from @article

👉 Rails maintains a list of all subscribes to a particular channel in the DB automatically. Broadcasts are also maintained automatically. In Rails 7, Redis was required. However, Rails 8 uses SQLite.

👉 Note that a client will not receive prior broadcasts but only those sent after it connects and subscribes to a channel.

Stimulus Javascript framework

Stimulus is a simple Javascript framework. It is designed to modify the HTML already on the page. The Stimulus code is written divided into Controllers. A Stimulus controller can be attached to an HTML element using data-controller attribute. Whenever there is a DOM event in the HTML like click or change, you can call a method from the Stimulus controller. Targets are elements of significance with the HTML which Stimulus controller needs to refer. Values are data stored in the DOM which a Stimulus controller can read, write, and observe.

Note that Stimulus connects to the HTML via data attributes only. In this sense it is unobtrusive and enables graceful degradation - your application will keep working even if Javascript does not load or has an error. 

Let us say, on your news website, you need to add a "Copy Link" button below the news story. You want it such that whenever user presses that button, that news story's URL should get copied to clipboard so that user can then paste it elsewhere. So, your HTML will look something like this:

<div class="news-story">
  ....
  <button class="copy-button">
    Copy Link
  </button>
</div>

Next, create a Stimulus controller

$ rails generate stimulus clipboard

The above command generates the following in app/javascript/controllers/clipboard_controller.js

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="clipboard"
export default class extends Controller {
  static values = {url: String}
  connect() {
  }
}

Next, connect the Stimulus controller to the relevant HTML element by adding a data-controller attribute

<div class="news-story" data-controller="clipboard">
  ....
  <button class="copy-button">
    Copy Link
  </button>
</div>

Whenever, Stimulus finds a the data-controller attribute, it creates an instance of the controller and attaches it to the HTML element and calls the connect method (which in this case does nothing). Next, we want to call a function whenever the button is clicked. So, we add a data-action attribute on the button which instructs stimulus to call clipboard controller's copy function on click. We should also provide the URL which will get copied to the clipboard by this copy function. That is provided by the data attribute added to the div as shown below:

<div class="news-story" data-controller="clipboard" data-clipboard-url-value="https://mysite.com/news/headline-1234">
  ....
  <button class="copy-button" data-action="click->clipboard#copy">
    Copy Link
  </button>
</div>

Now, we just need to implement the copy method in the stimulus controller:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="clipboard"
export default class extends Controller {
  static values = {url: String}
  connect() {
  }

  copy(){
    navigator.clipboard.writeText(this.urlValue);
  }
}

Note that copy is just a regular Javascript function. So, we can write the Javascript code to read the url and copy it to Browser's clipboard any way we want. But Stimulus helpfully provides a nice way to read data attributes and convert it to the correct data type. If you add a static values list of attributes you expect, like in the code above, Stimulus will helpfully add 3 helper methods: this.urlValue (get the value of first data attribute), this.urlValues (get an array of all data attributes) and this.hasUrlValue (whether this data attribute is present). Also, a callback method urlValueChanged can be defined which will get called whenever this data attribute changes.

Stimulus also supports target, class and outlet to let you reference important elements from the HTML. It is also possible to write data back into the data attributes to store state.

Turbo Native

Finally, Turbo Native is a framework that allows you to build hybrid mobile apps for iOS and Android. By default you get the whole web application packaged as a mobile app. Then it is possible to selectively build some pages using the native APIs to get the high fidelity feel of the native mobile app.

References


/ / /