Turbo is a large part of the Hotwire ecosystem, and Turbo Frames and Turbo Streams are two flavors of Turbo. At a high level, Frames and Streams achieve the same thing. Both allow us to manipulate elements on an HTML page, without a full page reload, in response to HTML sent “over the wire” from the server.
Same, but different
Upon initial inspection it can seem like they solve the same problem, but in practice Frames and Streams are very different. They work differently, and they have different use cases. Let’s dig into how these Turbo flavors are distinct. While both Turbo Frames and Streams let us manipulate elements on an HTML page, they differ in the following ways.
Turbo Frames only manipulate themselves, and this manipulation is always an update.
Turbo Streams, on the other hand, can manipulate any DOM element anywhere on the page. This manipulation can update existing content, remove elements, append or prepend content within an element, add HTML elements before or after a matching element in the DOM, or replace an element entirely.
And while both Turbo Frames and Streams respond to HTML sent “over the wire”, each triggers these responses with different events.
Frames and Streams: Events from the UI
For Turbo Frames, any request triggered by an interaction within the Frame will include a Turbo-Frame header, and therefore trigger a Turbo response from the server.
Note: these responses, regardless of what the server sends back, will only update the Frame itself. Whether the server sends an HTML fragment or a full HTML page, Hotwire is clever enough to automatically remove anything unnecessary (like layouts, for example) when handling a Turbo response.
Moreover, the data attribute can be used to update a particular Turbo Frame from outside the Frame itself:
Or to escape a Frame and trigger a full page reload:
Turbo Streams, similarly, can be sent from the server in response to user interactions, but these interactions can be from anywhere on the page, and the server response can update any content anywhere on the page. For example, a form submission to create a new item could send a Stream response from the controller#create method that appends the new item to a list of items elsewhere on the page.
Frames: On Page Load
With Turbo Frames, the src and loading attributes allow us to load a Frame independently of the initial page load.
This can happen through eager loading: for example, a Frame can fire a request to load its content straight after initial page load:
Or a with lazy loading, where it loads content only when the Frame itself becomes visible on the page
Note: Eager loading is the default here. The following are equivalent:
Streams: Server-side Events
Unlike Frames, Streams can be triggered by various server-side events. For example, Streams can be broadcast directly from Rails models. This is great for things like displaying a notification to the user when a particular model is updated or when a background job completes.
Note: Streams are typically triggered in response to POST or non-GET requests, unlike Frames which can be updated via GET requests (such as in the lazy loading example).
Streams or Frames?
How do we decide between using a Turbo Frame or a Stream? Let’s look at some use cases.
Turbo Frames are great when we want to divide a page into blocks of content that can be updated independently of the rest of the page. For example, we may use a Frame to display a list of items, and have the Frame update itself whenever a user filters the list. Modals are another good use case.
Eager and lazy loading with Turbo Frames is a good way to improve the initial page loading time. Lazy loading can improve page performance when there is a lot of content initially hidden from view, for example tabbed or accordion content, or content that is simply far beyond the current viewport.
Turbo Streams, by contrast, are great when we want to make multiple changes in multiple places on a page from a single request. For example, when we add a new item, we may want to append this item to the list of items, update the item count somewhere else on the page, and change a button state from disabled to active.
Turbo Streams are also good for making changes based on events happening server-side (and not directly from user interaction) such as displaying notifications.
As a general rule, we try to use Turbo Frames where possible and use Turbo Streams when Frames are not enough.
It’s Frames and Streams All The Way Down
Despite their differences, however, Frames and Streams can interact freely with each other. We could, for example, use a Turbo Stream to update content within a Turbo Frame, or indeed the entire Frame itself. Whether we should do this is perhaps another question, one of simplicity and best practice, but it’s good to be aware that this kind of interactivity is possible in Hotwire.
Over to you! Are you using Turbo in production? If so, how? What are your use cases, and in which situations do you prefer a Frame over a Stream? We’d love to hear from you.