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 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 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:
<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:
// 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. For full instructions, see turbo-rails
installation and Stimulus installation.
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:
- 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}"
). - Use a
lazy
frame for the next page so that it doesn't load automatically unless it comes into view.
<%= 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, but you can adapt it to any other pagination method.
We don't need anything special in the controller. A standard index
method works:
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.
<!-- 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:
// 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.
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
).
<!-- 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.
<!-- 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.
# 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.
# app/models/coment.rb # ... def stream broadcast_prepend_later_to(post, :comments, target: :comments, locals: { highlight: true }) end
<!-- 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.
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 and the Turbo documentation 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. 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:
Turbo::FramesHelper
for Turbo Frames.Turbo::Broadcastable
for broadcasting to Turbo Streams from the code.Turbo::Streams::TagBuilder
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!