I am going to report about my first experiences with Hotwire in this article which is also available in German.
My personal impression from the last few years is that modern web applications correspond to the following architectural approach:
- API-first approach on server side
However, many applications I’ve had the pleasure of encountering would not have required an SPA on the client side, and yet they were developed with Angular or React. Think about your recent projects for a moment: Did you consider using an SPA or did you jump straight to the framework question?
All code samples discussed in this article can be found on GitHub including a runnable sample project around the code snippets.
Hotwire consists of three components that can be combined well – but do not presuppose each other:
- Turbo: Turbo is the heart of Hotwire and the focus of this article. Turbo Drive replaces the navigation in classical web applications, so that a behavior analogous to SPAs is generated without complete reloading of the page when navigating. Using Turbo Frames, pages can be divided into smaller blocks that can be updated independently. Turbo Streams enable streaming updates of the content. So Turbo already does a lot of what Angular, Vue.js, React, etc. do.
- Strada: Strada is not yet released, but is intended to provide a way for native code and web views to interact in mobile apps.
Quarkus and Qute with Hotwire
Hotwire promises to work platform-independently – so it doesn’t matter what technology the backend is implemented with. Many examples I could find on the web about Hotwire are implemented either with Ruby or with Spring Boot (in combination with Thymeleaf for templating) in the backend. I am a big fan of Quarkus as an alternative to Spring Boot and therefore want to use it. With Qute, Quarkus brings its own templating engine. I won’t discuss Quarkus and Qute in detail in this article – but for understanding how Hotwire works, you don’t need a deeper knowledge of Quarkus and Qute, since all the functionality is achieved framework-independently through attributes in HTML.
The code samples for this article are from a demo application: it is a simple to-do app with in-memory data storage. To-dos can be displayed, added, marked as done and removed via the app.
Turbo is the heart of Hotwire. It handles a whole range of functions, which are described in detail below.
Turbo Drive: Navigation without reloading
Seamless navigation without reloading and with smooth transitions, as you are used to from SPAs, is the easiest thing to implement. All you have to do is to integrate Turbo into the application:
I integrated Turbo via unpkg inside a
<script> tag. Turbo Drive will then automatically take care of the navigation when clicking on links as well as form submissions that stay within the same domain (protocol/host/port combination). In the background, link clicks and form submissions are executed as XHR and the browser history is adjusted so that forward or backward navigation via the browser still works. As soon as the response to a request arrives, the page content that is currently displayed will be modified: the elements from
<head> are merged,
<body> is replaced with the received one. If a response takes longer than 500 ms, Turbo Drive automatically displays a progress bar so that users can see that the page is still loading. For an even better user experience, Turbo Drive tries to load the new page to be displayed from the cache and then updates it after the response arrives.
Turbo Frames: Frames with benefits
With Turbo Frames, you can divide a page into individual blocks, giving you even greater control over which parts of the page an interaction relates to. As an example, there is a list of to-dos to be displayed within a table:
To mark a part of the page as a Turbo Frame for Turbo, the
<turbo-frame> tag is used. To be able to identify each frame, you give them an ID (todolist). Turbo then ensures that interactions within the frame (here, for example, the interaction “Mark as done”) only refer to this frame. If a Turbo Frame with the same ID is returned in response to a request, this block is replaced and the rest of the page remains untouched, as you are used to from an SPA.
In the table, the to-do with ID 123 is now marked as done and the button is disabled.
Lazy loading – for free!
As a nice goodie, you get lazy loading from Turbo for free. When loading a page, you can deliver sections with a placeholder (here an empty
<div> that displays a spinner via CSS), which makes the page load faster and already displays elements that do not have a long load time.
The src attribute tells Turbo that a request should be executed. The response to the request is handled in the same way as we have already learned: if the response contains a Turbo Frame with the same ID, this block is replaced.
Updating frames from outside?
That works! For this purpose the attribute data-turbo-frame is used, which tells Turbo which frame should be updated.
If a response to the POST request to create a to-do contains a
<turbo-frame> with the ID todolist, this frame will be updated.
During my implementation, I put my foot in my mouth in the sense that updating a Turbo Frame from outside with the data-turbo-frame attribute doesn’t work if there is a
<div> with the same ID around the
<turbo-frame>. So the following example will not work:
All of it already feels very much like SPA behavior, right? But there’s more!
Turbo Streams: Even more dynamics
Turbo Streams can be used to send multiple actions for a website within one response to a request. For example, the response to a request to create a to-do may look like this:
Turbo Streams define a specific format for the response: each action to be performed on the website is enclosed in a
<turbo-stream> element. The action attribute specifies the action to be performed. The ID of the HTML element to be addressed with the action is defined in the target attribute. In this case, this does not have to be a Turbo Frame, but can be any HTML element. In the example, todoListTable is simply a tbody element:
<tbody id="todolistTable"> and no-todos is a row in a table:
There are currently seven actions supported by Turbo Streams: append, prepend, replace, update, remove, after, before. These actions do exactly what you would expect them to do. In our example, a row with the information about a to-do is appended to todoListTable and also the row with the ID no-todos is removed from the table. If new content is to be added or existing content is to be updated or replaced, it must be specified in a
Any number of
<turbo-stream> elements can be combined within a response. However, it is important that
text/vnd.turbo-stream.html is used as the content type. Turbo then does the rest. In the example implementation of the to-do app with Turbo Streams, the corresponding Turbo Frame is no longer updated, but the new row for the to-do is streamed into the table:
Modern web applications without an SPA? That’s possible! Thanks to Hotwire. The Basecamp team did an excellent job.
The SSR architectural approach means that templating is once again centralized in one place (just as it used to be with classical web applications). Also, thanks to the lightweight library Turbo, there is no need to wait for the SPA to be rebuilt again and several MBs to be reloaded in the browser. All in all, a great experience.
Integrating Hotwire into a Quarkus application with Qute worked without any problems. Anything else would have surprised me, since the main thing is to use the right HTML elements and set the right attributes.
Finally, I can only recommend you to gain experience with Hotwire (and especially Turbo) yourself. And maybe Hotwire is a good fit for your next project? It does not always have to be an SPA.
 HTML Over The Wire | Hotwire: https://hotwired.dev/ (last visited on August 24, 2022)