Turbo Streams on Rails

Updated March 17th, 2024 to account for the release of refresh and morph Stream actions in Turbo 8

Turbo, part of the Hotwire tripartite, helps you build modern, performant web applications with dramatically less custom JavaScript.

Turbo is composed of Turbo DriveTurbo FramesTurbo Streams, and Strada. Each is a valuable piece of the puzzle but today we’re going to focus on Turbo Streams. Turbo Streams “deliver page changes over WebSocket, Server-Sent Events, or in response to form submissions” and they might be the most exciting part of the Turbo package.

Streams allow you to deliver fast and efficient user experiences with minimal effort and, for Rails developers, you’ll find that streams fit comfortably into your existing toolkit. With just a few lines of code, you can send tiny snippets of HTML over the wire and and use those snippets to update the DOM with no custom JavaScript.

For fancier use cases, you can broadcast changes over a WebSocket connection to keep every interested user updated in real-time. No custom JavaScript to write, just HTML and a little bit of Ruby.

Let’s dive in.

What’s a Turbo Stream?

At their core, streams allow you to send snippets of HTML to the browser to make small changes to the DOM in response to events that happen on the server.

The basic structure of a Turbo Stream is an HTML snippet that looks like this:

<turbo-stream action="action_to_take" target="element_to_update">
  <template>
    <div id="element_to_update">
      <!-- Some more html -->
    </div>
  </template>
</turbo-stream>

The <turbo-stream> element always includes an action, from the following possibilities:

  • append
  • prepend
  • before
  • after
  • replace
  • update
  • remove
  • morph
  • refresh

In addition to an action, a target (or targets) must be provided for all actions except for refresh. Refreshing and morphing are special cases introduced in Turbo 8, and you can read more about morphing and Turbo Stream refresh broadcasts in this article.

Inside of the stream element, the HTML to render gets wrapped in a <template> tag. Turbo’s JavaScript processes these HTML snippets by reading the action and target from the stream tag and inserting (or removing) the wrapped HTML as appropriate.

Turbo Streams can be sent in response to a direct request from a browser (like submitting a form) or broadcast to subscribers over a WebSocket connection.

With turbo-rails, we have the tools we need to take either path, and we’ll look at both methods in detail next.

Turbo Streams in controllers

The simplest way to get started with Turbo Streams is to have a controller respond to turbo_stream requests and render a turbo_stream response in return.

This approach looks something like this:

# players_controller.rb
def create
  @player = Player.new(player_params)

  respond_to do |format|
    if @player.save
      format.html { redirect_to @player, notice: "Player was successfully created." }
      format.turbo_stream
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

# app/views/players/create.turbo_stream.erb
<%= turbo_stream.replace "players_form" do %>
  <%= render partial: "new_player" %>
<% end %>

In this example, a user is attempting to save a new player record in the database.

When the save is successful and the request format is turbo_stream, we use Rails’ implicit rendering to respond with create.turbo_stream.erb which renders a Turbo Stream response with a replace action targeting the players_form element.

The turbo_stream method in the create view generates the necessary <turbo-stream> tag and wraps the content within the turbo_stream block in a <template> tag.

The response back to the browser will look like this:

<turbo-stream action="replace" target="players_form">
  <template>
    <!-- Content of new_player.html.erb partial -->
    <turbo-frame id="players_form">
      <a href="/players/new">New Player</a>
    </turbo-frame>
  </template>
</turbo-stream>

Turbo’s JavaScript then steps in, processes the response, and updates the DOM. We do not need to make any adjustments to the failure path in our controller above. Even though the form submission is a turbo_stream request, Rails falls back to responding with HTML when a turbo_stream format to respond with does not exist.

Our example demonstrates handling a POST request with Turbo Streams, but we can use the same approach with a GET request, add data-turbo-stream to the link or GET form you want to send as a Turbo Stream request and Turbo will handle the rest.

Rendering inline

For simple responses we do not always need dedicated .erb files. Instead we can render the turbo_stream response inline in the controller, like this:

def create
  @player = Player.new(player_params)

  respond_to do |format|
    if @player.save
      format.html { redirect_to @player, notice: "Player was successfully created." }
      format.turbo_stream { render turbo_stream: turbo_stream.replace('players_form', partial: 'new_player') }

    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

There is no functional difference between inline rendering and rendering from a template, for simple actions you can use whichever feels better.

Updating multiple elements at once

The create.turbo_stream.erb file we saw earlier renders a single <turbo-stream> tag, but we aren’t limited to updating a single stream per response. We can update multiple elements at once by simply adding them to our turbo_stream.erb file:

<%= turbo_stream.replace "players_form" do %>
  <%= render partial: "new_player" %>
<% end %>
<%= turbo_stream.append "players" do %>
  <%= render partial: "player", locals: { player: @player } %>
<% end %>

Now after a successful form POST, we will send back a response with two <turbo-stream> tags, each of which will be processed by Turbo’s JavaScript on the front end.

If you really dislike creating turbo_stream templates, you can also render multiple Streams inline in the controller like this:

format.turbo_stream do
  # Pass an array of turbo_streams to the render call
  render turbo_stream: 
    [
      turbo_stream.replace('players_form', partial: 'new_player'),
      turbo_stream.append('players', partial: 'player', locals: { player: @player })
    ]
end

Should you do this? Probably not, but it works.

Broadcasting changes

So far, we have seen rendering streams from controller actions. This method works great for dealing with form submissions made by individual users; however, there is another method of broadcasting updates to a wider audience that you can call on when needed.

As we saw in the Hotwire introduction video, stream broadcasts sent from the controller only update the page that made the original request. If other people are viewing the same page, they won’t see the update until they refresh the page.

If your use case requires every interested user to see changes as they happen, you can use broadcasts from the turbo-rails gem to send streams to every subscriber at once, in real time.

The basic syntax for these updates looks like this, in your model:

# app/models/player.rb
after_create_commit { broadcast_append_to('players') }

Here we’re using a callback, fired each time a new player is created, to broadcast the newly created player to the players channel.

Broadcast options

By default, the Stream target will be set to the plural name of the model; however you can override the target as needed by passing in a target to the method, like this:

after_create_commit { broadcast_append_to('players', target: 'players_list') }

Broadcasting from the model will attempt to use to_partial_path to guess the name of the partial that should be rendered.

In our examples so far, if we have a partial in app/views/players named _player.html.erb that partial will be used. As with targeting, you can override the partial like this:

after_create_commit { broadcast_append_to('players', partial: 'a_special_player') }

When streaming in this manner, you must create a WebSocket connection to the channel you’re broadcasting updates on.

To do that, you can add <%= turbo_stream_from "some_channel" %> to the view. The name of the channel passed to turbo_stream_from must match the name of the channel passed to broadcast_action_to , otherwise your updates will get lost in space.

broadcast_action_to vs. broadcast_action_later_to

In the source code for broadcastable, a comment near the top of the file advises us to use broadcast_action_later_to instead of broadcast_action_to .

Using _later methods moves the stream broadcast into a background job, which means that broadcasts can happen asynchronously, moving the potentially expensive rendering work out of the web request.

Since we want everything to be fast, not slow, we’ll use broadcast_action_later_to, like this:

after_create_commit { broadcast_append_later_to('players') }

The only exception to the later rule is delete actions. broadcast_remove_to simply removes an element from the DOM without rendering a template and so does not need to be moved into a background job. To reinforce this exception, broadcast_remove_later_to is not defined and calling it will throw an undefined method error.

Broadcasting from a controller

So far we’ve seen that we can render a Turbo Stream response from a controller action and broadcast from a model.

Another, less common, path is to call broadcast_append|remove|replace on an instance of a model from within a controller (or a service, or anywhere else you like), like this:

@player.broadcast_append_later_to("players")

This does the same thing as calling this method in a callback in the model, meaning the append will broadcast to all users subscribed to the players channel.

More magical methods

So far we’ve been adding _to at the end of our broadcast methods and explicitly passing in the channel name. If you like magic, you can omit the _to from your broadcasts and just use broadcast_append|remove|replace.

Using this form requires a channel streaming from the model instance, like the below.

# player.rb
after_update_commit { broadcast_replace }

# _player.html.erb
<%= turbo_stream_from @player %>

To type even less, you can add broadcasts_to to your model, like this:

# player.rb
broadcasts_to ->(_) { 'players' }

Here we’re using a non-standard stream name of players. If we’re using the model instance as the stream name, we can get down to a single magic word:

# player.rb
broadcasts

Both broadcasts and broadcasts_to automatically add broadcasts on createupdate, and delete commits to the model, as seen here

In practice, these methods often do not add much value since they rely so much on magical naming convention.

The magic fails when attempting to, for example, append a newly created record to an index list. In that scenario, our stream won’t be tied to an instance of the class, and so we’ll need to use the long form version of broadcast_action_to, like we saw above.

When in doubt, just use the longer, slightly less magical methods. I’m sharing these magic methods because they’re commonly used in guides and feature prominently in the announcement video so you’ll likely find them in the wild for years to come. If your use case allows you to use them, go for it, but the longer versions work just fine too.

Gotchas

Scope your channels!

In most of the examples I’ve shown so far, we are broadcasting to streams with hardcoded strings for names (“players”). This works fine; however, in most web applications resources aren’t globally available. Account 123 should only see updates on their account, not every account. You can avoid streaming updates to the wrong user by ensuring you’re broadcasting to properly scoped channels and subscribing users to those scoped channels.

In the players example code we’ve used so far, we can imagine that a player belongs to a team, and updates to those players should be broadcast on the team channel.

To implement this in our code, it might look like this:

# teams/show.html.erb
<%= turbo_stream_from @team %>

# player.rb
belongs_to :team

after_create_commit { broadcast_append_later_to(team) }
after_destroy_commit { broadcast_remove_to(team) }

With this in place, the team show page subscribes to updates related to that specific team, keeping data from leaking between different teams. The same concept can be applied to scope channels by user or account, as needed.

Note that this scoping is only necessary for broadcasts sent over WebSocket. If your controller renders a turbo_stream response, only the client that made the initial request will receive the update. There is no risk of data leaking in this manner when responding to a form submission with a turbo stream template.

Error handling

When you render a turbo_stream from a controller action and the DOM doesn’t have a matching target element to update… nothing happens. There’s no error anywhere, the stack trace indicates that the partial was rendered, and the DOM doesn’t update.

This is normal, expected behavior. There was not an error, there just wasn’t anything to change in the DOM - but it can be confusing when you’re getting started and the lack of errors can make troubleshooting more difficult.

If you are not getting errors, the turbo_stream response is showing in the server logs, and nothing is updating the page, the problem is (almost) always that the target passed to <turbo-stream> isn’t matching any elements in your page’s markup. Start by checking what you are rendering on the page and compare that to the target passed to the stream.

Wrapping up

Turbo is an incredibly powerful tool, and the tight integration between Rails and Turbo means that forward-looking Rails developers should be exploring ways to bring Turbo into their applications.

Using streams with turbo-rails opens up a world of possible user experiences for Rails developers that previously meant more work, more effort, and more code to maintain.

Hopefully what I’ve shared here helps you feel a little more comfortable thinking about streams, and how streams can fit into your existing code base. As always, thanks for reading! More resources Ready to dive deeper?

  • Spend some time with turbo-rails source code
  • Spend some time with the turbo source code
  • Dive into the docs
  • Read some practical guides, like this guide on building a live search interface, or this one on handling modal form submissions
  • GoRails has published a variety of Hotwire-specific guides

Finally, if you want to learn about implementing Turbo Streams in a complex Rails application, I wrote a book on building a modern Ruby on Rails application with Hotwire. The book is written in this same, step-by-step tutorial style that you find throughout this blog and aims to offer solutions for a variety of common, real-world web application problems without reaching for the weight of a full frontend framework.

Hotwiring Rails newsletter.

Enter your email to sign up for updates on the new version of Hotwiring Rails, coming in spring 2024, and a once-monthly newsletter from me on the latest in Rails, Hotwire, and other interesting things.

Powered by Buttondown.