in

A modest critique of Htmx

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.

Inheritance of
Htmx properties is a definite mistake
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.

Most
interesting web apps cannot replace wholesale a DOM element
Because DOM elements almost always have browser-local state, such as
the open/closed state of a element, the
input of an 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.

Storing
state in the DOM element itself is a bad idea
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.

The default queuing mode is
bonkers
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 are non-local
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.

Component state
cannot be maintained very well
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.

We’ve done some unsatisfying work with WebComponents, but those
things have bizarre limitations that are surprising.
We’ve also made a bridge directly to React components that we use
from our server-side language, but in general Htmx and React fight for
control over the flow of state and management of DOM elements.
We’ve played with Alpine, which is nice, but it represents yet
another client-side-programming library and is therefore redundant
if React is already in your codebase.

The up sides
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:

No serialisation from our DB types to frontend types is needed.

No data leaks, and no GraphQL needed.

We can use our (in our opinion) more powerful abstraction facilities
of the server-side language.
We can use the form builder in our server side language; instead of
doing one frontend and also one backend implementation of the
same validations.

But the above downsides are real.

Htmx-in-React?
An attractive future direction might be to re-implement Htmx in
React:

The server sends a JSON blob that React converts into virtual DOM
components.
That would solve the component state problem.
It would mean we require no special bridge to use React
components.
It would let us use our React-connected web fetching library, and
carefully avoid the queuing choices made by Htmx.
It would solve the morphdom problems and browser DOM input elements
problem, too, which is pretty much a solved problem 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.

Report

What do you think?

Newbie

Written by Mr Viral

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

Stay somewhere off the beaten path with the best camping deals this Prime Day

Stay somewhere off the beaten path with the best camping deals this Prime Day

Instant (YC S22) is hiring a founding engineer to help build a modern Firebase