Rails 7: Turbo Your Rails •WIP•

HotWire and Turbo Rails are now first-class citizens in Rails. That means that Hotwire is the default way to implement a Rails app. If you are using Rails as an API-only backend, this likely does not affect you. However, it’s important when structuring a new Rails app to think about how much navigational control you will want to delegate to Rails’s Turbo system and/or how much you want to implement as a single-page app (for example, using a front-end routing system instead of Turbo).

In this crash minicourse I’ll cover Rails 7 only, its major parts and key elements benefits, whats new, what’s changed, and why. Although Rails remains an excellent option for API-only backends (create your rails app with --api-only), Rails 7 major improvements are around the frontend and the new Turbo, Stimulus, and Hotwire paradigms. It is not recommended that you mix these paradigms with Javascript-heavy frontends, like React, Angular, Vue, etc. It’s either—or, and here’s my brief recommendation:

Most dashboard-like apps, apps like Facebook, Github, Shopify, Basecamp, etc will fly just fine using Rails Turbo. Paradigmatically, your business logic will remain on the server (primarily), and you won’t write much business logic into Javascript or the frontend layer.

That will cover 80% of apps out there. These apps need a lot of quick user interaction that treats the client as a dumb terminal and leaves the view rendering to Rails.

The kinds of apps that Turbo Rails will not be suitable for are apps which are necessarily data-heavy on the frontend or apps that require specific animations when flipping between views. (Turbo Rails handles your transitions and the options are limited.)

For example: charts & graphs, interactive gaming experiences, super interactive/immersive VR experiences, highly immersive mobile or browser experiences. Also, apps that need specific animations between steps (like games). For these apps, you need the data to be very close to the user, and you want to harness that fact to build highly interactive or animated experiences. For these kinds of apps you might do best to consider a more data-centric frontend paradigm.

Unless you want to split you app into different sections, I would not recommend mixing & matching React, Vue, Angular, etc with Turbo Rails. Turbo Rails is an all-in solution that takes over all link clicks and forms on your website. You must know how it works or else you will be thrown off and get unexpected errors. In this lesson, I will cover Turbo Rails, Hotwire, and Stimulus to set you up for success with Rails 7.

(For my apps, sometimes I build do build some sections of the app in React while still using Turbo Rails for the primary navigation. However, I am very careful to make sure to separate my React parts from the Turbo Rails interactions and it takes more work. To find out my about this, see Option B in my Rails Heart React Series.)


What is Turbo Rails?

Turbo Rails is a collection of Javascript that operations behind the scenes and also an implementation of pattern for how Rails responds to requests on the backend

Importantly, our Rails app is now made up of turbo-frames, that look like this:

<turbo-frame id="new_message">
<form action="/messages" method="post">
...
</form>
</turbo-frame>

Here’s the explanation of what a Turbo Frame is, from the Turbo manual:

Turbo Frames allow predefined parts of a page to be updated on request. Any links and forms inside a frame are captured, and the frame contents automatically updated after receiving a response. Regardless of whether the server provides a full document, or just a fragment containing an updated version of the requested frame, only that particular frame will be extracted from the response to replace the existing content.

https://turbo.hotwired.dev/handbook/frames

• Turbo will intercept your links and forms. Yes, you! Yes, all of them. Unless you explicitly specify to disable it, all of your forms sent to the backend and links to new pages will operate differently from a traditional web app. They will be sent as TURBO_STREAM requests and Turbo will manage the page to swap out the relevant pieces of content.

In the Rails console that looks like this:

When you submit the form, the controller will process a TURBO_STREAM response.

Importantly, when there’s a failure, Rails now sends back an HTTP status code 422 (Unprocessable Entity).

A standard Rails respond block in a Turbo app might look something like this:

respond_to do |format|
format.turbo_stream
format.html
end

You’ll need to have partials that end with .turbo_stream.erb

The trick to using Turbo Stream:

In the partial, make sure that you are explicitly naming a replace whose id exactly matches an existing turbo-frame on the page.

Do I have to use Turbo Rails?

You do not, and for a Rails API-only application, it probably is a good idea to disable it.

Tubro Streams

Turbo Streams are places in your app where you want Rails to latch onto the HTML, and using websockets (action cable), replace an area of the app via a server push— that is, something that happens on the server’s side.

To achieve this, follow these steps

Step 1: From Any Object, Call broadcast_replace_to

You’ll want to use either an explicit method called after an object is updated (for example, using after_save_commit) or during a background job.

broadcast_replace_to takes one positional parameter: self, and three named parameters (target, partial, locals).

In this example, I’m broadcasting to an object called Thing.

For target, use something namespace to the area of the app you’re in, a double underscore, and #{dom_id(self)}. Separate everything with a single underscore except the space between the namespace and the thing, which you separate with a single underscore.

For the partial, you’ll include the path to where the partial will get rendered from, as we’ll set up in step 2.

For locals, you will generally always pass thing: self (or whatever the name of the thing is).

      broadcast_replace_to self,
                           target: "account_dash__#{dom_id(self)}",
                           partial: "account_dash/things/line_inner",
                           locals: { thing: self }

To get dom_id working inside of a model, be sure to add this line to the model

include ActionView::RecordIdentifier

Step 2: Setup the ‘inner’ partial

I like to have a partial called ‘inner.’ This is conceptually important because it is used twice: once upon a natural render and again upon a server-pushed rerender through a turbo stream.

In this example, the inner partial is at account_dash/things/_line_inner.erb

<%= turbo_frame_tag "account_dash__#{ dom_id(thing) }" do  %>
  <div class='row' data-id='<%= thing.id %>' data-edit='false'>
    <%= render partial: 'account/things/show',
               locals: { thing: thing } %>
    last updated <%= Time.current %>

    <div class=" scaffold-line-buttons col-md-2" >

        <%= link_to "Edit Thing".html_safe, account_dash_thing_path(account,thing), 'data-turbo' => 'false', disable_with: "Loading...", class: "edit-thing-button btn btn-primary btn-sm" %>
    </div>
  </div>
<% end %>

Here, we have only the row that gets updated, a namespace within a turbo_frame_tag that matches exactly the target we specified in Step 1.

We then render a partial account/things/show (not shown here), along with an edit button.

Step 3: The Naturally Rendered Line

This view I call ‘naturally rendered’ because it is the part of the page that is rendered only once, but where we declare that we need a turbo stream. Specifically, we use a turbo_stream_from tag with no close (end) to it. It simply should exist at the top of the partial that you want to be updated when that thing publishes an update.

In this example, you find this in a view file at views/account_dash/things/_line.erb

<%= turbo_stream_from thing  %>
<%= render partial:  "account/things/line_inner", locals: {thing:  } %>

Working bottom-to-top, this gets included from a parent view views/account_dash/things/_list.erb

<% things.each do |thing| %>
  <%= render partial: 'account/things/line', locals: {thing: thing} %>
<% end %>

And finally, at the top of the view nesting, we have the views/account_dash/things/index.erb

<%= render partial: 'account_dash/things/list',   locals: {things: @things}) %> 

Your controller is responsible for generating the @things instance variable.

Your controller will be named AccountDash::Things and would need to have an index method.