The Initial Confusion with Hotwire
When I first found Hotwire, the documentation threw me off a bit. I couldn't understand the difference between Turbo Frames and Turbo Streams. They both seemed to do the same thing, but being a part of a Ruby community for the past 15 years and knowing we're all fans of the Don't Repeat Yourself principle, I was 100% sure I was missing something.
Fast forward a few months of using Hotwire, it became clear to me why Hotwire was built the way it was, and in this video, I will explain when you should pick one over the other. To help you visualize these concepts better, I'll use this animation which I borrowed from my practical Ruby on Rails for beginners class, and then we'll look at a practical example.
High-Level Request/Response Flow
To give you a little bit of context, I'm using a simple controller where actions and templates are named first, second, and third. And that's why in the address bar you see first, because that is the starting page which loads the HTML elements that the user can interact with. In this case, it's the button. And then when the user performs an action like submitting a form or clicking a button, a request goes out to the server, and then the response comes back to the browser which updates the page.
So in the case of this Turbo Frame, the server responds with a new frame which replaces the contents of the existing frame, displaying a message. And the same goes for Turbo Streams. A request gets sent to the server, the server sends back a response, and the browser updates the page. So at this point, you probably see why I got confused. It's because the differences are not obvious at first. You have to dig a little deeper to see why both Turbo Frames and Turbo Streams exist.
Upgrading a Page with Turbo Frames
Let's look at Turbo Frames first. Turbo Frames are great for taking an old-style full HTML page and turning it into a modern, responsive looking page in minutes. All you have to do is wrap the slice of the page you want in a Turbo Frame tag. That's it.
Let's take this page as an example which doesn't have any Turbo Frames. Whenever you click this button, it takes you to the second page, and you can see that the response is another full page with headers, body, and everything else as you would expect. It's a completely new page, even though because of Turbo Drive it's a bit faster to render, but otherwise there is nothing special going on. What we want to achieve with Turbo Frames is we want the contents of the second page to be loaded within the bottom half of the first page. In other words, we want to re-fetch the contents of the second page whenever we click the button.
Alright, so let's add a Turbo Frame. Now I will wrap everything in a frame because the button needs to be inside the frame that we want to update. And since we want to update the contents of the div with the id of content, we've got to wrap everything in a frame. And we're going to do the same for the response. The response needs a frame with the same identifier as the one that originated the request. In this case, it's going to be frame_one. So after clicking the button, we can see that the response doesn't contain the entire page anymore, it only returned the contents of the template. This is great because there is a lot less content to send over the wire. But there's a small problem with this setup, namely the fact that the link has been replaced with the contents of the second template. That's because the response updated the entire frame, not just the part we wanted to update.
Targeting Specific Frames for Updates
Fortunately, this is easy to fix. We can set the data-turbo-frame attribute on the link to explicitly tell Turbo which frame to target when the response comes back from the server. So we'll wrap the second div in a frame tag and we'll set the data-turbo-frame attribute to the frame's identifier, namely frame_one. And if we retry in the browser, the behavior is correct now. The button is still on the page and the second half of the screen is now populated with the new content every time we click the button.
That's amazing. It feels like a single page app. And notice how easy it was to add a Turbo Frame. Just a few wrapping tags and you're done. That's a big benefit that Turbo Frames have over Turbo Streams. Another benefit is you can lazy load page segments as they become visible, and you can also cache the frames independently of each other.
Limitations of Turbo Frames
But there are three problems with Turbo Frames. The first problem is we can only update one frame at a time. This means that if we wanted to click a button and update two or more sections of the page, it wouldn't work. The second problem is that we can only update the frame's contents. We cannot delete, prepend, append, or anything other than just update. The third problem is that we cannot update the page programmatically by sending new content from the back end. And that's where Turbo Streams come into play.
Introducing Turbo Streams for Granular Control
They don't have those limitations, but there is a small price to pay. You've got to write more code to wire everything up. So let's see how we could replace our Turbo Frame with Turbo Streams. I will remove the frames from both the first page and the second. So now we're back to our link taking us to a new page. Cool.
As I said, there is more code you have to write to wire Turbo Streams up. Also, you gotta use non-GET requests like forms instead of links. This limitation will probably be removed soon, but at the time of this recording, it's still there. So I'll replace the link_to helper with button_to, which generates a form, and the form will point to the third action, which needs to be a post route instead of a get route. Lastly, we need to return a response. And the response uses these Turbo Stream messages to tell the browser what to do. In this case, we want to update the contents of the content div.
So if I try this in the browser by clicking the button, you'll see the response looks a bit different. It's got a different tag now, it's turbo-stream instead of turbo-frame, and it's got an action and a target. The action is update and the target is our div ID. But we could have more than one action as opposed to Turbo Frames where you only have the update option. We could add some content before the div. You could also append, prepend, replace, remove, add content after the element, and also you could target multiple elements with one message. So there are a lot more options available with Turbo Streams than there are with Turbo Frames.
Real-Time Updates with WebSockets
But that's not all. One of the significant differences is you can update the page via WebSockets. This means you can open up a WebSocket connection in your browser and then you can send data through that connection from anywhere in your application. It could be a controller, a model, or even the Rails console. So let's set it up and try it.
I'll add a new post route named fourth which we'll trigger using our button. And inside the fourth action, we can use the Turbo::StreamsChannel module to send Turbo Stream messages over the WebSocket connection. So let's just broadcast updates as we did before. We'll update the content div through the my_stream stream. Clicking the button sends a post request to the fourth action which broadcasts a message through the my_stream stream to all browsers connected to it. And we can even stream from the console. Here I'm using the same Turbo Streams Channel module to call broadcast_update_to with the same stream name and target, but instead of rendering a partial, I'm using some inline HTML. And if I had a model, I could do the same from a model callback or any other method.
Summary of Differences
So let's summarize the differences. In terms of how easy they are to implement, Turbo Frames win because there is not that much you have to do. You just wrap sections in frames and you're done. But updating multiple sections at the same time is only supported by Turbo Streams, which also has WebSocket support, and it can do a lot more than just updates. On the other hand, Turbo Frames are also suitable for progressively loading frames when a particular section of the page becomes visible. Caching is another benefit that Turbo Frames bring to the table because it allows you to split the page into multiple smaller page segments, each of which can be cached independently of the others.
If you want to see another cool example of what's possible with Hotwire, you should check out this video where I use React together with Hotwire.