//

Hotwire: A new (old) approach for modern web applications

30.8.2022 | 11 minutes of reading time

Hotwire (HTML over the wire) was introduced by Basecamp in late 2020 and promises to be an alternative approach to developing modern web applications with less JavaScript:

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire. [1]

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
  • Single-Page Application (SPA) on client side, which generates HTML from JSON responses from the backend and JavaScript on client side (no matter if Angular, Vue.js or React, …)

One of the most common arguments for the use of this architectural approach and against the use of Server-Side Rendering (SSR), i.e. the generation of HTML on the server side, is the supposedly better user experience with SPAs: SPA frameworks load the content of a web application dynamically. Under the hood, however, only a JavaScript API is used for this and this API is not only usable by SPAs. But the issue of usability is also a major risk when using SPAs: Some of the JavaScript libraries that have to be loaded in the browser are enormous, thus leading to very long loading times and non-usability especially on mobile devices with poor reception, among other things. In addition to this fact, the use of this architectural approach blurs the separation of responsibilities for appearance and behavior, presentation logic and templating, routing, business logic, and maintaining the application’s state. [2] If the SPA has to be available offline, all of the above topics must be implemented (at least partly) in the client. In “classical” architectural approaches with SSR, only the appearance and behavior is located in the client, everything else is handled by the backend. Offline capability, by the way, is a good reason to use an SPA.

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

Hotwire is a collection of JavaScript modules that produce the behavior of an SPA (e.g. dynamic loading) for classical architectural approaches with SSR. Hotwire uses an HTML-first approach, so the backend returns HTML instead of JSON.

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.
  • Stimulus: Stimulus can be used to develop so-called controllers that provide the functionality of a custom element. HTML elements are enriched with JavaScript logic in an HTML-first approach (e.g. the logic to connect to Server-Sent Events EventSource can be bound to a button).
  • 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

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:

1<script crossorigin="anonymous" src="https://unpkg.com/@hotwired/turbo@7.1.0/dist/turbo.es2017-umd.js"></script>
2

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 Drive can be controlled in even more detail using attributes and additional JavaScript: Please take a look into the handbook for all possible configurations. For the sample application I used the default settings without any further configuration.

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:

1<turbo-frame id="todolist">
2   <table>
3       <thead>
4           <tr>
5               <th>Title</th>
6               <th>Completed</th>
7               <th>Mark as done</th>
8               <th>Remove</th>
9           </tr>
10       </thead>
11       <tbody>
12           <tr>
13               <td><a href="/todos/123">Todo</a></td>
14               <td>Open</td>
15               <td>
16                   <form action="/todos/123" method="POST" enctype="multipart/form-data">
17                       <input type="submit" value="Done">
18                   </form>
19               </td>
20               <td>
21                   <form action="todos/remove/123" method="POST" enctype="multipart/form-data">
22                       <input type="submit" value="Remove">
23                   </form>
24               </td>
25           </tr>
26       </tbody>
27   </table>
28</turbo-frame>
29

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.

1<turbo-frame id="todolist">
2   <table>
3       <thead>
4           <tr>
5               <th>Title</th>
6               <th>Completed</th>
7               <th>Mark as done</th>
8               <th>Remove</th>
9           </tr>
10       </thead>
11       <tbody>
12           <tr>
13               <td><a href="/todos/123">Todo</a></td>
14               <td>Done</td>
15               <td>
16                   <form action="/todos/123" method="POST" enctype="multipart/form-data">
17                       <input type="submit" value="Done" disabled>
18                   </form>
19               </td>
20               <td>
21                   <form action="todos/remove/123" method="POST" enctype="multipart/form-data">
22                       <input type="submit" value="Remove">
23                   </form>
24               </td>
25           </tr>
26       </tbody>
27   </table>
28</turbo-frame>
29

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.

1<h1>TODO List</h1>
2<turbo-frame id="todolist" src="/todos">
3   <div class="loader"></div>
4</turbo-frame>
5

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.

1<h1>TODO List</h1>
2<turbo-frame id="todolist" src="/todos">
3   <div class="loader"></div>
4</turbo-frame>
5
6<h1>Create TODO</h1>
7<form action="/todos" method="POST" enctype="multipart/form-data" data-turbo-frame="todolist">
8   <label for="name">TODO:</label>
9   <input type="text" id="name" name="name" required>
10   <br>
11   <input type="submit" value="Create">
12</form>
13

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:

1<div id="todolist">
2   <h1>TODO List</h1>
3   <turbo-frame id="todolist" src="/todos">
4       <div class="loader"></div>
5   </turbo-frame>
6</div>
7
8<h1>Create TODO</h1>
9<form action="/todos" method="POST" enctype="multipart/form-data" data-turbo-frame="todolist">
10   <label for="name">TODO:</label>
11   <input type="text" id="name" name="name" required>
12   <br>
13   <input type="submit" value="Create">
14</form>
15

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:

1<turbo-stream action="append" target="todolistTable">
2   <template>
3       <tr id="123-row">
4           <td><a href="/todos/123">Todo</a></td>
5           <td>Open</td>
6           <td>
7               <form action="/todos/stream/123" method="POST" enctype="multipart/form-data">
8                   <input type="submit" value="Done">
9               </form>
10           </td>
11           <td>
12               <form action="/todos/stream/remove/123" method="POST" enctype="multipart/form-data">
13                   <input type="submit" value="Remove">
14               </form>
15           </td>
16       </tr>
17   </template>
18</turbo-stream>
19
20
21
22<turbo-stream action="remove" target="no-todos">
23
24</turbo-stream>
25

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: <tr id="no-todos">
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 <template> element.
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:


As an alternative to Turbo Streams as responses to user interactions such as form submissions, Turbo Streams can also be transmitted through event streams. Such a stream must be explicitly registered with Turbo via a JavaScript API (is this the first time we’re writing JavaScript?), but no longer requires the content type to be set during event submission. Turbo assumes that the events are interpretable as Turbo Stream actions. In the sample application, Server-Sent Events (SSE) are used as the event stream, but you could use WebSockets just as well.

1<script type="text/javascript">
2   if (window["EventSource"] && window["Turbo"]) {
3       Turbo.connectStreamSource(new EventSource("/todos/sse/connect"));
4   } else {
5       console.warn("Turbo Streams over SSE not available");
6   }
7</script>
8

This small JavaScript section is enough to register the EventSource with Turbo. On the backend side, there are no other special requirements there are just normal SSE or WebSocket connections. Only the format of the events must conform to the specified format of Turbo Streams. For demo purposes, a new to-do is created here from outside the client and the update appears on the website:

Final thoughts

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.

Personally, I’ve had a lot of fun developing with Hotwire. Since a large part of the configuration is already done in HTML, there is no need to go deep into a JavaScript API. I was especially surprised by Turbo Streams: Without having to write much JavaScript, you can easily stream updates to a website.

A bit more of JavaScript is needed for Stimulus, another component of Hotwire. Stimulus simulates quite simplified custom elements, but again with a clear focus on HTML first. I deliberately didn’t describe Stimulus in more detail because I would personally go straight to custom elements to stay as close to the web platform as possible. Stimulus is well documented in its own handbook .

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.

References

[1] HTML Over The Wire | Hotwire: https://hotwired.dev/ (last visited on August 24, 2022)
[2] JavaScript? Yes, but in moderation: https://www.innoq.com/en/articles/2020/01/javascript-in-ma%C3%9Fen/ (last visited on August 24, 2022)

share post

Likes

0

//

More articles in this subject area\n

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.