We’re big fans of Hotwire. Hotwire is a collection of technologies that let us send HTML (rather than JSON or XML) “over the wire” giving us the look, feel, and speed of a single-page web application, without any custom JavaScript.
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:
data: { turbo_frame: "my_frame_id" }
Or to escape a Frame and trigger a full page reload:
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:
<turbo-frame id="greeting" src="/greet" loading=”eager”></turbo-frame>
Or a with lazy loading, where it loads content only when the Frame itself becomes visible on the page
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
Perhaps now we have a clearer understanding of the distinction between Turbo Frames and Turbo Streams. While both achieve a similar goal, partial page updates without custom JavaScript, they are independent of each other and have quite different use cases.
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.
*Reference: Dohmke, T., Iansiti, M., Richards, G. (2023). Sea Change in Software Development: Economic and Productivity Analysis of the AI-Powered Developer Lifecycle
Talking Turbo
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.