Ruby on Rails has been around for a long time. During this time plenty of other web development frameworks have come and gone. I can think of many reasons for its endurance, but being able to adapt to the latest trends of web development is definitely near the top of my list.
One of the biggest advantages of Rails is that it is still used by its creators to solve their own problems. Rails was developed to build Basecamp and as Basecamp grew in features, so did Rails. Recently, Rails' creators set out to build a full email system called Hey, and this lead to the second giant leap in Rails' abilities.
The recent rise of Single Page Applications (SPA) was mostly driven by various Node.js frameworks. In essence, an SPA is driven by a frontend that pulls JSON payloads from the backend and modifies the DOM on the frontend, creating a fast and seamless experience.
From the early days, Rails was best at rendering pages on the server-side. Building SPAs with Rails usually meant rendering JSON payloads and merging them with a boatload of JS on the frontend to take care of the rest. The first consequence of this approach is that most existing Rails apps need a great deal of refactoring. You also need to choose a frontend JS framework and redo a lot of your pages to deal with the JSON payloads, which is not easy.
Near the end of 2020, DHH and the folks at Basecamp released three new tools that, when combined, take Rails to the next level. Collectively they are called Hotwire.
While there are some very good examples of Rails + Hotwire out there, there are not many that get to the details of how these pieces work and how they can be used in your day to day projects.
In this post, I want to take some time and introduce the different components of Hotwire and how they work with Rails.
What is Hotwire?
First, what are Hotwire components:
The first piece is Turbo. Turbo is the heir to Turbolinks that was released with earlier versions of Rails. Originally Turbolinks was responsible for fetching the links on a page, caching them, and replacing the page with the cached content to make the application seem more responsive. The new Turbo is somewhat similar, but does a lot more and is much more useful.
Turbo itself consists of 2 components: Frames and Streams. More on those later.
The second piece of Hotwire is Stimulus - an opinionated and minimal JS framework that is very good at manipulating static HTML DOM without too much load and a "holistic" approach.
The third component of Hotwire is Strada which is not fully released yet and is responsible for building native experiences on mobile devices. For the purpose of this post, I'm going to focus on the first 2 components.
Turbo Frames
Hotwire is based on a different approach than other SPA frameworks. Instead of rendering JSON down the wire and rendering that JSON on the frontend, it deals with fully rendered HTML. This makes using Hotwire with existing Rails apps much easier.
Let's start with a simple:
<%= button_to switch_path do %>
<%= switch.state %>
<%= hidden_field_tag :state, switch.state %>
<% end %>
In this example, you click the button and its state goes from On to Off. With every click, the page is reloaded. Now let's add Turbo Frames.
For this to work, we're going to need the hotwire-rails gem.
What do Turbo Frames do?
In Rails, Turbo Frames are rendered by using the turbo_frame_tag
in your views. Once they wrap around any links or form submits, they hijack the link or the submission to the server. The server gets the same request and is not aware that the request is not from the page, but the Turbo frame. It therefore renders the page as it would have normally and sends a full page HTML back. However, the Turbo frame now receives the response and looks for a Turbo Frame in it with the same name and, once it finds it, replaces it with the new Turbo frame without reloading the page.
<%= turbo_frame_tag :switch do %>
<%= button_to switch_path do %>
<%= switch.state %>
<%= hidden_field_tag :state, switch.state %>
<% end %>
<% end %>
That's all it took to turn a full page reload into a partial page update with Turbo frames.
Notes on Turbo Frames
- For new payloads to replace the old one in a frame, the payload should contain a Turbo Frame with the same name.
- With all links hijacked inside of a frame, you won't be able to navigate anywhere outside of it. If you want a link to actually navigate out of a frame, add a
target="_top"
to your frame. - Without extra parameters Turbo Frames don't change the loading of your page. Everything will render and load as before but wrapped inside of a
turbo-frame
element.
Loading your page in the background
If parts of your page are slow to load, you can use Turbo Frames to improve the usability for the user. Wrap the slow part in a turbo_frame_tag
like the last time, but also add a src
attribute to it as well.
Let's say your page has 2 parts:
<%= render 'fast_part' %>
<%= render 'slow_part' %>
The second file renders fast, but once the page is loaded, Turbo Frames makes a call to the server to fetch the second part. This makes the page load much faster for the user.
Notes on lazy loading frames
- Avoid using lazy loading when the final content is going to move the rest of your page down. For example, if a top frame is lazy loaded, it will push down the rest of the page when it's fully loaded and will cause the page to jiggle.
- If you have parts of the page that are not visible at first load (below the fold or hidden by default), you can use
loading: :lazy
in your frame. This will not fire up the request until the frame is in view. - Make sure that your payload for the frame (
slow_part_path
in the example above), includes a Turbo Frame with the same name (part_2
in the example above). - You can place a wait spinner in your frame to spin while the frame is loading.
Turbo Streams
Turbo Streams are the second part of Turbo. They are responsible for sending payloads through WebSockets (ActionCable in Rails) even when the client has not initiated the call.
Take the following example: you want to update stock tickers on a page that also contains other elements. Normally you'd use WebSockets and ActionCable channels to do this. However with Turbo Streams this becomes much easier when mixed with Turbo Frames.
In its most basic form, you can initiate a fully rendered HTML partial to be sent down to a spacific Turbo Frame on a page. Combined with Turbo Frames and with deep integration into ActiveRecord, Turbo Streams can improve your Rails application's user experience by a huge margin.
The Hotwire Rails integration (through the hotwire-rails
gem) also includes additions to ActiveRecord. These additions mean you can send updates to the frontend when changes occur in your ActiveRecord (or ActiveModel) objects.
Let's see an example:
<%= turbo_frame_tag dom_id(stock) do %>
<%= stock.ticker %> is <%= stock.price %>
<% end %>
Let's start with stock.rb
. You notice broadcasts
there. This comes from the Turbo Streams Broadcast extensions to ActiveRecord. By adding broadcasts
to your model, you're telling Turbo Streams to render a partial and send it to anyone who's listening on the frontend. The name of this partial, by default is _stock.html.erb
(the name of your model, but beginning with _
as it is a partial). Every time the object is updated (or a new one is created), this file is rendered and sent to all Turbo Frames with the right name. This name by default is the result of dom_id
function. As you can see in the example above, by wrapping the price viewer in a Turbo Frame with the right name, we're going to get an update pushed to any client that's listening when the Stock ActiveRecord object is modified.
Of course you can limit this by using the various Broadcast methods, only to send the rendered payload based on specific ActiveRecord hooks.
Notes on Turbo Stream Broadcasting
- Since Turbo Stream updates initiated by
broadcasts
are generated on the server side, they are not going to have any global variables available to them. This means you shouldn't use variables likecurrent_user
(from Devise) or railsCurrentAttributes
there. Sessions are also not available on those partials. See this post on Hotwire and Devise for more on how to make those variables work. - The payload sent down to the Turbo Frames should have the same name as the Turbo Frame.
- Make sure to include some user or account identifiable part in your frame and stream names, otherwise you might send updates to the wrong client. For example you can use something like
<%= turbo_stream_from Current.account, :stock %>
.
In the next post, I'm going to dive deeper into Stimulus, the JS part of Hotwire.