This is the second post in the series “Have Clojure UIs Taken the Wrong Path?”. The first post is here.
How do we choose between building an application using a hypermedia approach versus the client-side SPA? This post won’t answer that question. But it will say how not to make that choice.
A common heuristic is the spectrum that stretches between old fashioned “Multi Page Apps” (MPAs) for basic use cases, to Single Page Applications for rich client interactivity.
MPAs are the “old fashioned” web applications that serve HTML to the browser. Client-side single page applications (SPAs) use RPC. The backend serves both some content as data, as well as a custom JavaScript application. Depending on how much interactivity you want your app to have, you select from along the spectrum. It’s a matter of tradeoffs.
Any time you are told that “it’s a matter of tradeoffs”, alarm bells should go off. It’s a good indicator that the framing of the question is fundamentally misguided. Good software design increases your options. But when the menu of options decreases optionality, it’s a good idea to stop and consider whether the entire approach is wrong. Posing the choice between something that is faster to build but less capable (MPAs) or more capable but complex (SPAs) is just such an example.
As Clojurians, shouldn’t we be suspicious of an approach that argues that sophistication must be purchased at the price of simplicity and data-orientation? The SPA-RPC approach, as I pointed out in part 1, is not simple. It couples UI and domain concerns (by definition!). It does so by abandoning hypermedia’s commitment to data-orientation. Instead of shipping information and controls as data, we need megabytes of JavaScript without which that data is of no use.
The constraints that cause the tradeoffs are often entirely accidental to the problem space. They reduce our options. They are antithetical to simplicity and data orientation. What if, instead, we could take an entirely different approach that offers simple, extendable, composable parts? What if we could opt in to greater sophistication only where we need it? What if we could minimize and localize complexity rather than building it in to our client-server architecture?
Lofty goals. But the MPA-SPA spectrum is wrong-headed in still other ways. It’s not the case that SPAs enable maximal client functionality. In fact, it is often the need for extensive, app-like interactivity rules out client-side SPAs. An example will be useful to illustrate this point.
Client Interactivity
I once built a browser-based live sports telestration application. Telestration involves adding animations onto a screen. Here’s a demonstration of some of its features.
This involves some pretty complex client functionality. The animations must be aware of the players on the field. The spinning circles should be under the players’ feet, flat on the field. The arrows to be aware of the angle of the field, even when the camera pans. Users can voice over the video. When they export the video, the audio of their voice is combined with the video and the animations. While users are editing the videos, they want to be able to preview the animations and undo mistakes. The application is used both in live settings (team practices, Zoom calls), and to export to videos that can be shared. Teams don’t always have great wifi, so the client needs to be able to recover from failed network connections.
This is not a case for React. React is far too slow to sustain a good framerate on a high definition video. The state includes every individual pixel on the original video, plus every pixel on the animations, plus the audio from the microphone. The logic involves applying algorithms on each frame to determine where players are, and what the lay of the field is relative to the camera. Some of this is in JavaScript, some is in WebGL.
Nor are frameworks without React’s performance overhead sufficient. The SPA clients are not a fit because of the complexity of the client state. The state management and logic is tied closely to the video frames, audio devices, and WebGL.
What lesson can we draw? Maximal client-side interactivity requirements are likely to disqualify JavaScript SPA frameworks.
Line of Business Applications
Let’s go to the other end of the spectrum and look at line of business applications. Suppose we don’t need any complex client interactivity. Is it really a good idea to develop these using plain HTML? Here too the spectrum of tradeoffs fails as a useful guide.
Users don’t want full page reloads every time they submit a form. They like live updates that don’t require page refreshes. Inline editing, dynamic data tables, and infinite scroll all make applications more appealing. However much developers are told that the UI doesn’t need to be pretty, it is inevitably the case that quality is judged largely on appearance.
The via vetus not only offers a poor experience for end users but also lacks modern affordances like hot reload.
Wouldn’t it be nice where we had an incremental approach? One that begins easy, with the affordances users and developers have come to expect? One that allows us to add sophistication only where we need it? Shouldn’t we look for an architecture that lets us move to the right as we need to?
Instead of architectures that constrain our options, why not seek architectures that maximize our options? Here we return to the notion of extended hypermedia.
Hypermedia is not HTML
HTML is a successful hypermedia. But it is important not to confuse HTML with hypermedia.
Let’s reconsider the definition offered by Roy Fielding:
When I say hypertext, I mean the simultaneous presentation of information and controls such that the information becomes the affordance through which the user (or automaton) obtains choices and selects actions. Hypermedia is just an expansion on what text means to include temporal anchors within a media stream; most researchers have dropped the distinction.
Interactivity is built in to the definition of hypermedia. That’s what makes it hyper.
For the rest of this series, we’ll use hypermedia in the following sense. Something is hypermedia if and only if:
-
It is transmitted from a server to a client
-
It includes all the information and controls the user needs as data
-
The client has no implementation-time knowledge of the domain
Data Oriented
It is data oriented. By contrast, consider a Reagent application. In order for the user to see or do anything, the browser receives two things from the server: data (usually in JSON or EDN form), and code (usually minified JavaScript). On its own, the data response is not useful. It needs a custom client with an understanding of the domain entities.
A hypermedia approach, on the other hand, sends the application as data to a generic client. It encodes both the information the user needs to see, but also – crucially – the controls: what the user can do.
Simple
Out of this data orientation arises simplicity. The hypermedia client is completely decoupled from the application domain.
The problem is that the range of hypermedia supported “out of the box” by the browser is very limited. It provides a poor experience to users, and isn’t much better for developers.
Extended Hypermedia
There is nothing intrinsic about hypermedia – or HTML – that requires full page reloads or that prevents infinite scrolling. The lack of support is just an implementation detail of the browser. Rather than using the browser as a runtime for a custom client app, we can extend the browser’s capabilities as a hypermedia client. We need not leave the hypermedia model to supplement the browser’s support for interactivity.
We have various means to extend the browser as a hypermedia client. One of those is JavaScript. HTMX is written in JavaScript, yet it extends the hypermedia model.
Consider an example where we have a task status, and we want it to update without refreshing the page. In HTMX, we can do something like this:
<span hx-get="/task/TSK001/status" hx-trigger="every 10s">
Assigned
</span>
The client will poll the backend every 10 seconds to see if the task status has changed.
Let’s review our criteria:
✅ Interchange from server to client
✅ Data oriented. Everything the user needs is shipped in band as data.
✅ No implementation time knowledge is needed on the client-side.
But didn’t we add HTMX? Isn’t that a client-side change?
This gets at the key point: the browser has been extended with HTMX. JavaScript is not, therefore, opposed to a hypermedia architecture. JavaScript is a powerful way to extend the browser as a hypermedia client!
But JavaScript is not the only extension mechanism. What if we need custom DOM elements? Custom web components let us create new, custom DOM elements. What if we need offline functionality? Recall that one constraint of REST has to do with layered systems. Service workers can act as a layer to return HTML when the network is down.
There are also server-side SPAs that I would include within the notion of extended hypermedia, though they do not observe all of the REST constraints. Phoenix Liveview is the front runner. Liveview is an SPA, but one where the state is controlled on the server side. As the state changes, efficient HTML diffs are sent over a websocket, and Morphdom is used to quickly modify the parts of the DOM that need to change.
Clojure has some libraries that take the Liveview approach. Ripley is one such library.
(def counter (atom 0))
(defn counter-app [counter]
(h/html
[:div
"Counter value: " [::h/live {:source (atom-source counter)
:component #(h/html [:span %])}]
[:button {:on-click #(swap! counter inc)} "increment"]
[:button {:on-click #(swap! counter dec)} "decrement"]]))
Notice that the counter
atom and the swap operations are on the server side.
The One True Way
The point here is not to say that HTMX, web components, service workers, or server-side SPAs are “the one true way”. The way I am advocating is not tied to a particular technology, but a particular way of evaluating technologies.
- Choose the options that increase your options
- Use simplicity, loose coupling, and high cohesion to avoid complexity
- Prefer data-orientation
Is hypermedia the only way to do this? Of course not. But using the browser as an extended hypermedia client has a lot going for it. It’s easy to get started with, and you can extend it in almost any way you need. It is very well understood. Great tooling exists. Documentation abounds.
I have presented a few options for extending the browser as a hypermedia client. The purpose was not to recommend, say, HTMX or a Clojure Liveview implementation. It was simply to convey some small sense of the wide range of options, and to advocate for a strategy of maximizing those options. Future articles may go into greater detail. But the world of hypermedia is larger than most think, and the range of applications that lend themselves to SPA-RPC is smaller.