Turbo Streams on Rails09 Aug 2021
Turbo is composed of
Turbo Streams, and
Turbo Native. Each is a valuable piece of the puzzle but today we’re going to focus on
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.
One of the challenges of new web development tools is that documentation can be sparse, and that’s no different with Hotwire and Turbo.
Official documentation exists, and the introduction video demoing Hotwire does a good job showing how Hotwire works, but comprehensive guides and documentation clearly outlining the various options you have when using Hotwire aren’t all the way there yet.
Streams are powerful but, with limited documentation, getting up to speed can be daunting.
So, today I’m going to share a bit of what I’ve learned from using Turbo Streams in a variety of applications, from small experiments to production-grade applications, to help you get value from Turbo Streams in your Rails application faster.
If you aren’t using Rails, some of the core concepts might still be helpful, but my focus will be on using streams within a Rails application via the turbo-rails gem.
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> element always includes an action, from the following possibilities:
In addition to an action, a target (or targets) must be provided.
Inside of the stream element, the HTML to render gets wrapped in a
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:
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
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
The response back to the browser will look like this:
Magically, we don’t 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.
For simple responses, we don’t need dedicated .erb files.
Instead we can render the turbo_stream response inline in the controller, like this:
There’s no difference between inline rendering and rendering from a template, for simple replace and append actions you can use whichever feels better.
Updating multiple elements at once
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:
Now after a successful form POST, we’ll send back a response with two
If you really dislike creating
turbo_stream templates, you can also render multiple Streams inline in the controller like this:
Should you do this? Probably not, but it works.
So far, we’ve seen rendering streams from controller actions. This method works great for dealing with form submissions made by individual users; however, there’s 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:
Here we’re using a callback, fired each time a new player is created, to broadcast the newly created player to the players channel.
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:
Broadcasting from the model will attempt to use to_partial_path to guess the name of the partial that should be rendered.
For our examples so far, if we have a partial in
_player.html.erb that partial will be used. As with targeting, you can override the partial like this:
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 at the top of the file advises us to use
broadcast_action_later_to instead of
_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:
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:
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
Using this form requires a channel streaming from the model instance, like the below.
To type even less, you can add
broadcasts_to to your model, like this:
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:
broadcasts_to automatically add broadcasts on
delete commits to the model, as seen here
In practice, these magic methods often don’t 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.
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.
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:
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.
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 wasn’t 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 aren’t 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’re rendering on the page, and compare that to the target passed to the stream.
Turbo streams don’t need turbo frames
In reality, streams don’t care about frames at all.
Streams can target any old DOM id you like and you can build an application entirely without frames and still get plenty of value from streams. Often times you’ll be targeting a turbo frame with a stream, but you’re free to target any element you like.
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.
In production applications, I have seen form submission round trips reliably complete in less than 200ms with turbo streams, all while writing code that is easy to understand, maintain, and scale as application requirements change.
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!
Ready to dive deeper?