At work, we really like the basic simple idea of Htmx. Based on using Htmx in a non-trivial user interface, across a team, we’ve found that the following cases are actually not simple and are quite complicated.
Across pieces of code, it’s very surprising and it’s implicit. Like in CSS, inheritance is a cheap hack but you get what you pay for.
It contradicts the author’s reasonable argument of locality of behaviour. It’s not local, it comes from all the way up there or some other module. Pretty much dynamic binding.
Default inheritance differs across various properties
(e.g. hx-delete
is not inherited, but
hx-confirm
and hx-ext
are). So you have to
remember these exceptions and you end up just being explicit about
everything, which means inheritance is pointless.
Because DOM elements almost always have browser-local state, such as
the open/closed state of a <details>
element, the
input of an <input>
element, the open/close state of
a dropdown element (which, note, is not encoded by an attribute of the
element when you click it). All of this state is lost if you replace
outerHTML directly with the naive happy path of Htmx.
Even morphdom overwrites some things you’d expect it not to, so we had to patch it to avoid messing with input elements, and details elements.
Morphdom is intended to correct the pains of the previous heading, but we discovered that the way that Htmx works assumes it’s based on replacing elements wholesale: it stores the request queue for the element on the DOM element itself. When you kick off a request, either from this element or from another that points to it, you have a request queue. Some bad failure modes are avoided by wholesale-replacing the DOM element, as the queue is reset. But with morphdom, the queue is retained because the element is retained. You’re now in a sort of undefined behavior land, where the designs of Htmx are violated.
By default, Htmx will cancel requests that are in-flight if you trigger another request on the same queue (element). That’s the default strategy. We discovered this afterwards. It’s highly unintuitive, it meant we were losing work.
Event triggers often help to make things happen, but they’re a non-local effect, and suffer from similar issues as property inheritance. A bit of DSL work in the server-side language can help with this, but it feels like old-school JavaScript callback-based programming to some extent; where you “subscribe” to an event happening and do something.
A broader problem, similar to the DOM element state issue, is that your own components have their own state. E.g. if you want a page that consists of three sections that have their own state that the server needs (e.g. which page of a set of results) and state that some e.g. React or WebComponents need, then you have a problem of synchronising state between a parent component and the child component.
Htmx does not provide a good story for this. We have some ideas, but they all have big caveats: use query parameters, use hidden form inputs, use event triggers.
React and Halogen (see also Halogen is better than React at everything) do have an answer to this. In both cases, child components have their own state, and parents can give them “props” which are pretty much “advice”, and they also have their own internal state, and can choose to ignore/take precedence over props. The props are typically sourced from the server or derived from the server, and the state is usually some client-side state.
We often do need to use React for off-the-shelf components or components that we have to use that are just provided as React. React and Htmx do not interact nicely.
Our current thinking is that being able to use your server side language is a huge obvious and uncontroversial win, no one on the team would want to go back to writing all this business logic in TypeScript:
But the above downsides are real.
An attractive future direction might be to re-implement Htmx in React:
In this way, we could drop the Htmx dependency but retain the benefits of the idea. That is, given a budget to embark on such a big piece of work.