Get Started with Hotwire in Your Ruby on Rails App
Tags: ruby, rails
Published: 2022-07-13 11:26:31

Hotwire is a hot topic at the moment for every Rails developer. If you work with Rails, there is a good chance you have already heard a lot about it.

Hotwire is a completely new way of adding interactivity to your app with very few lines of code, and it works blazing fast by transmitting HTML over the wire.

That means you can keep your hands clean from most Single Page Applications (SPA) frameworks. You can also keep your rendering logic centralized on the server, while still maintaining quick page load times and interactivity.

In this post, we'll look at the main components of Hotwire and how to use it in your Rails app. But first: what is Hotwire and why should you use it?

## What Is Hotwire?

Hotwire is not a single library, but a new approach to building web and mobile applications by sending HTML over the wire. It includes Turbo, Stimulus, and Strada (coming later this year).

We will discuss each of these in detail in the next section.

_Side note_: While Hotwire is highly linked with Rails, it is completely language-agnostic, so it can work just as well with other applications.

I have been using Stimulus in production on several non-Rails apps and some static websites. You can use Turbo without Rails as well.

But let us come back to the Rails world for now.

## Why Use Hotwire in Your Rails App?

So when should you use Hotwire? The answer is anywhere you want to add interactivity to your application. For example, if you want:

- Some content to be displayed/hidden conditionally based on a user's interaction (e.g., an address form where the list of states automatically changes based on the selected country).
- To update some content in real-time (e.g., a feed like Twitter where new Tweets automatically get added to the page).
- To lazy-load some parts of your pages (e.g., inside an accordion, you can load the titles and mark the details to be lazy-loaded to speed up load times).

## Hotwire Components

As mentioned before, Hotwire is a collection of new (and some old) techniques for building web apps.

Let's discuss each of these in the next few sections.

### Turbo

HTML drives Turbo at its core.

Turbo provides several techniques to handle HTML data coming over the wire and display it on your application without performing a full page reload.

It is composed of:

- _Turbo Drive_

  If you have used [Turbolinks](https://github.com/turbolinks/turbolinks) in the past, you will feel right at home with Turbo Drive. At its core, some JS code intercepts JavaScript events on your application, loads HTML asynchronously, and replaces parts of your HTML markup.

- _Turbo Frames_

  Turbo Frames decouple parts of your markup into different sections that can be loaded independently. For example, if you have a blog application, the content of your post and the comments are two related but independent parts of the page. You can decouple them so navigation works independently or even load them asynchronously with turbo frames.

- _Turbo Streams_

  Turbo Streams offers utilities to easily bring in real-time data to your application. For example, let's say you are building a news feed like Twitter. You want to pull new tweets into a user's feed as soon as they are posted without reloading the page. Turbo Streams allow you to do this without writing a single line of JS.

- _Turbo Native_

  Turbo Native lets you build a native wrapper around your web application. Navigations and interactions will feel native without you having to redo all the screens natively. You'll keep delivering the rest of the application through the web. That way, you can focus on the really interactive parts of your application and get them right.

### Stimulus

[Stimulus](https://stimulus.hotwired.dev/) is a JavaScript framework for writing controllers that interact with your HTML.

Let's say we need to add some JavaScript attributes like `data-controller`, `data-action`, and `data-target` to elements on a page. We'll write a stimulus controller with access to elements that receives events based on those attributes.

Here's an example:

```html
<div data-controller="clipboard">
  PIN:
  <input data-clipboard-target="source" type="text" value="1234" readonly />
  <button data-action="clipboard#copy">Copy to Clipboard</button>
</div>
```

It is very easy to get an idea about what this does without even reading the associated Stimulus controller.

Here's a controller that goes with the HTML:

```javascript
// src/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["source"];

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value);
  }
}
```

That is at the core of Stimulus: keeping things simple and reusable.

Now, if you ever need a copy-to-the-clipboard button on another page, you can just re-use that controller. Add the `data-*` attributes on the markup to get everything working.

### Strada

Unfortunately, we don't know much about Strada yet. But it will allow a web application to communicate (and possibly perform actions) with a native app using HTML bridge attributes.

## How to Use Hotwire in Your Ruby on Rails Application

I don't want to spend too much time discussing Hotwire installation or a basic use case. The Hotwire team has already done an excellent job of it in their [Hotwire screencast](https://hotwired.dev/). For full instructions, see [`turbo-rails` installation](https://github.com/hotwired/turbo-rails#installation) and [Stimulus installation](https://stimulus.hotwired.dev/handbook/installing).

Let's jump straight into some common Hotwire use cases.

### Endless Scroll

Using Turbo Frames, we can easily make a page with automatic pagination as the user scrolls. For this, we need to do two things:

1. Render each "page" inside its own frame by appending the page number to the frame id (e.g., `turbo_frame_tag "posts_#{@posts.current_page}"`).
2. Use a [`lazy` frame](https://turbo.hotwired.dev/handbook/frames#lazy-loading-frames) for the next page so that it doesn't load automatically unless it comes into view.

```erb
<%= turbo_frame_tag "posts_#{@posts.current_page}" do %>
  <%= render @posts %>
  <% unless @posts.last_page? %>
    <%= turbo_frame_tag "posts_#{@posts.next_page}", :src => path_to_next_page(@posts), :loading => "lazy" do %>
      <%= render "loading" %>
    <% end  %>
  <% end  %>
<% end %>
```

Note that this example uses methods from [Kaminari](https://github.com/kaminari/kaminari), but you can adapt it to any other pagination method.

We don't need anything special in the controller. A standard `index` method works:

```rb
class PostsController < ApplicationController
  def index
    @posts = Post.page(params[:page]).per(params[:per_page])
  end
end
```

The trick here is that we use nested frames, with the frame for the next page nested inside the frame for the previous page.

That way, when the first page loads, the frame for the next page is placed at the end. When the user scrolls to that frame, it is replaced with the content of the second page. The lazy frame for the third page renders at the end.

### Dynamic Forms

You can easily implement dynamic forms with Hotwire without custom logic for toggling fields on the front end. This is a bit more involved than the endless scroll use case, as it includes the use of both Turbo Stream and Stimulus.

Let's start with our form first.

```erb
<!-- app/views/posts/new.html.erb -->
<div data-controller="refresh-form" data-refresh-form-url="<%= refresh_form_posts_url(:target => "new_post") %>">
  <%= render "form" %>
</div>

<!-- app/views/posts/_form.html.erb -->
<%= form_for(@post, :data => { :target => "refresh-form.form" }) do |f| %>
  <%= f.select :kind, options_for_select([["News", :news], ["Blog", :blog]], @post.kind), {}, data: { action: "change->refresh-form#refreshForm" } %>

  <%= f.select :category, options_for_select(categories_for_kind(@post.kind), @post.category) %>
<% end %>
```

The form is simple enough — we display a `kind` select with `News` and `Blog` options. We want to change the available categories' values based on the kind that is selected (assuming that `categories_for_kind(@post.kind)` returns the list of categories for the given kind).

If you look closer, you'll see that we've added some data attributes to the form.

The `data-target` will link the form element to the `RefreshFormController` Stimulus Controller's `form` target.
And the `data-action` with the value of `change->refresh-form#refreshForm` will call the `refreshForm` method on the linked Stimulus Controller every time the `kind` select is changed.

Let's look at our Stimulus Controller:

```js
// app/javascript/controllers/refresh_form_controller.js
import { Controller } from "stimulus";
import { put } from "@rails/request.js";

export default class extends Controller {
  static targets = ["form"];

  refreshForm() {
    put(this.data.get("url"), {
      body: new FormData(this.formTarget),
      responseKind: "turbo-stream",
    });
  }
}
```

On all `refreshForm` calls, we just make a new `PUT` request to the controller's URL (set using the `data-refresh-form-url` on the same element with a `data-controller="refresh-form"`).
The important part here is that the `responseKind` is set to `turbo-stream`.

The `@rails/request` library understands this response and performs instructions based on the response stream.

Now all that's left is to return the correct stream from our `refresh_form` call for Turbo to understand and update our form.

```rb
class PostsController < ApplicationController
  def refresh_form
    @post = Post.new
    @post.attributes = post_params
    @post.valid?
    respond_to do |format|
      format.turbo_stream
    end
  end
end
```

Just update the attributes on the post and mark that you want to respond in a `turbo_stream` format (so that it looks up `refresh_form.turbo_stream.erb`).

```erb
<!-- app/views/posts/refresh_form.turbo_stream.erb -->
<%= turbo_stream.replace params[:target] do %>
  <%= render "form" %>
<% end %>
```

In this step, we are reusing our `form` partial, wrapping it inside a `turbo_stream` with a `replace` action.

And that's all you need to get a dynamic form working.

I know this looks a bit advanced, but the `refresh` stimulus controller is a shared part you can now use for all your dynamic forms by adding the correct `data-*` attributes.
So essentially, you now get server-side dynamic form refresh without writing any new JS for other forms. Pretty awesome, right?

### Append Content to Pages Without Reloading

The next use case that Hotwire makes easy is streaming HTML over a WebSocket connection and updating a page with new content as it comes in.

A good example of this is the GitHub comments section.
You can implement this very easily using Turbo Streams.

There are two parts to this.

First, we embed a turbo stream listener on the listing page that opens a WebSocket connection to the server and listens for events.

```erb
<!-- app/views/comments/index.html.erb -->
<div id="comments">
  <%= turbo_stream_from @post, :comments %>

  <% @comments.each do |comment| %>
    <%= render comment %>
  <% end %>
</div>
```

Next, we update the model to broadcast new comments to the stream.

```rb
# app/models/coment.rb
class Comment < ApplicationRecord
  belongs_to :post

  after_create_commit :stream

  private

  def stream
    broadcast_prepend_later_to(post, :comments, target: :comments)
  end
end
```

You don't need anything else. Turbo will automatically render the `app/views/comments/_comment.html.erb` partial for each new comment and send it over a WebSocket connection. It will be picked up by Turbo's JS and prepended to the target with id `comments`.

Let's go one step ahead and add an indication to all newly added comments with a small Stimulus Controller.

First, modify the broadcast and `comment` partial to include the controller conditionally.

```rb
# app/models/coment.rb
# ...
def stream
  broadcast_prepend_later_to(post, :comments,
                             target: :comments,
                             locals: { highlight: true })
end
```

```erb
<!-- app/views/comments/_comment.html.erb -->
<div <%= %s(data-controller="highlight") if local_assigns[:highlight] %>
  <%= comment.body %>
</div>
```

This small Stimulus controller adds a special highlight class on connection for 3 seconds and then removes it.

```js
export default class extends Controller {
  connect() {
    this.element.classList.add("highlight");
    this.timeout = setTimeout(
      () => this.element.classList.remove("highlight"),
      3000
    );
  }

  disconnect() {
    clearTimeout(this.timeout);
  }
}
```

_Note_: You also need to update the CSS highlighting based on the presence of that class.

Once this controller is done, you can re-use it on anything that requires a highlight class. You could even modify it to get the duration and class name from data attributes if you need that flexibility.

That's the great thing about Hotwire — it takes you a long way, and you don't have to dip your hands in JS. When you do need to write some JS, Stimulus gives you the tools to build small generic controllers that can be re-used.

## Wrap Up and Further Reading

The Rails community has been really excited with the introduction of Hotwire, and rightly so.

In this post, we looked at the key components of Hotwire and how to use Hotwire in your Rails app. We touched on how you can bring your application to life using Turbo and Stimulus.

The [official Hotwire screencast introduction](https://hotwired.dev/#screencast) and the [Turbo documentation](https://turbo.hotwired.dev/handbook/introduction) are great places to see what Hotwire and Turbo can do for you.

For advanced usage, I suggest heading over to the [turbo-rails GitHub repo](https://github.com/hotwired/turbo-rails).
Sadly, the documentation is a bit sparse, but if you are not afraid to get your hands dirty, read the code and inline comments in:

1. [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb) for _Turbo Frames_.
2. [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb) for broadcasting to _Turbo Streams_ from the code.
3. [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb) for broadcasting to _Turbo Streams_ as part of inline controller actions.

Happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**