A critical, deeply technical analysis of why htmx is an architectural mismatch for stateful design systems. We'll deconstruct the "DOM as State" anti pattern with code snippets that illustrate the resulting network chattiness, event complexity, and performance issues.
A system architect’s most crucial responsibility is to select the right tools for the job. This requires a deep and impartial analysis of the problem, an openness to advice from subject matter experts, and a genuine exploration of all viable possibilities. When this process fails, and a tool is chosen based on hype rather than suitability, the technical consequences can be severe.
This post is a deeply technical analysis of one such failure: the decision to use htmx, a server focused tool, for the fundamentally client focused problem of a stateful design system.
The Core Mismatch: DOM as State versus State as Truth
The fundamental conflict lies in where the application's state resides. HTMX's philosophy is that the state lives on the server, and the DOM is a direct reflection of that state, updated by swapping HTML fragments. The DOM effectively becomes your client side state machine.
Modern JS frameworks like React operate on the opposite principle. The source of truth is a virtual, in memory state. The DOM is merely a render target, a final output of that state. For complex UIs, treating the DOM as your state is a known anti pattern. It is slow, difficult to debug, and creates a cascade of predictable and damaging technical problems.
A Technical Breakdown of the Failures
When you force a "DOM as State" tool to build a stateful component library, you encounter a series of severe technical hurdles.
1. Inefficient State Management via Network Requests
A stateful component, like a simple counter, should update its state instantly on the client. With htmx, this trivial action requires a full server roundtrip.
Consider a simple counter component. In htmx, the implementation looks like this:
<div id="counter" hx-target="this" hx-swap="outerHTML">
<span>Count: 0</span>
<button hx-post="/increment/0" class="btn">+</button>
</div>
[HttpPost("/increment/{currentCount}")]
public IResult Increment(int currentCount)
{
var newCount = currentCount + 1;
var html = $"""
<div id="counter" hx-target="this" hx-swap="outerHTML">
<span>Count: {newCount}</span>
<button hx-post="/increment/{newCount}" class="btn">+</button>
</div>
""";
return Results.Content(html, "text/html");
}Every single click triggers a network request just to increment a number. This creates a sluggish user experience and couples the component's UI directly to a specific backend endpoint.
Now, compare this to a React component handling the same logic:
// The React Component
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>Count: {count}</span>
<button onClick={() => setCount(count + 1)} className="btn">+</button>
</div>
);
}The state is managed instantly on the client side with zero network latency. The component is entirely self contained, making it truly reusable.
2. Brittle Communication via "Event Hell"
A design system requires components to communicate. In htmx, this relies on a web of custom DOM events. If Component A needs to update Component B after an action, the server's response for Component A must include an HX-Trigger header. A completely separate Component B must then have an hx-trigger="my-event from:body" attribute to listen for it.
This becomes an unmaintainable web of loosely coupled events. It is impossible to trace the data flow, there is no type safety, and debugging becomes a nightmare of inspecting network headers and DOM event listeners. This is a fragile and brittle way to build a complex system compared to passing a strongly typed callback function as a prop.
3. The "No JavaScript" Fallacy
For any non trivial component, the promise of "no JavaScript" quickly evaporates. You are inevitably forced to write JavaScript to handle complex client side logic using the htmx.on() event listeners. At this point, you are writing imperative DOM manipulation, but without the structure, tooling, and vast ecosystem of a proper JS framework. You get the worst of both worlds: a messy hybrid of declarative HTML attributes and unstructured JavaScript.
Conclusion: The High Cost of the Wrong Architecture
While htmx is an excellent tool for its intended purpose, it is architecturally unsuited for building stateful, client centric design systems. Forcing it into this role leads to severe and predictable consequences: poor performance, a frustrating user experience, and a brittle, unmaintainable codebase that slows all future development to a crawl.
It serves as a firm reminder that an architect’s most critical job is to understand a tool's core philosophy and recognise, with technical certainty, when it is the wrong tool for the job.
