I’ve just completed a port from HTMX to Hotwire (Stimulus, Turbo). HTMX is a great idea, but in my experience it’s a poor execution.
It’s really quite buggy, in my experience it plays poorly with fundamental web and browser features (relative links are broken in at least 2 ways, I fixed a third way). One of the events just stopped working at all in the most recent release. The docs are lacking. And where it promises to let you write less JS, if you ever do need to write some JS you’re on your own in structuring that, and you’ll be fighting against HTMX (who gets to update the DOM, maintaining event handlers, etc).
As a (brief) contributor to HTMX, I also feel like these issues were all inevitable. It’s a single 5k line file with 190 top level functions in it meaning it’s pretty impenetrable to get up to speed on. When proposing a bug fix the maintainers weren’t sure if it would have other consequences. Tests didn’t cover the functionality. I’ve been mostly a backend engineer in my career, and I empathise with not wanting the complexity of a modern frontend, but that doesn’t mean we can’t have some basic organisation of the code to make it approachable and more obvious whether changes will work or not.
After porting to Turbo and Stimulus I have a more reliable code base, I have significantly less JavaScript, and I have a JS code base that much easier to reason about. I really wanted to like HTMX but the execution is not there. A focus on stability is a great fit for the project, but it’s most certainly not there yet and has quite a way to go in my experience.
Hey Dan, htmx maintainer here.
I would love to know in what other two ways relative links are broken, and which event stopped working, so we can get those fixed. With respect to the fix you PRed (thank you, by the way), we did get that merged very quickly, and would love to do the same for whatever is broken here, even (especially) if you are no longer interested in doing the fix yourself. [0]
As for the DX of making changes to htmx: absolutely true! The single file is a DX we've chosen, and it has both costs and benefits, which we've written about in "Why htmx doesn't have a build step". [1]
Hi Alex, thanks for the reply and for your work on HTMX. I love the idea and think there's a strong need for something like HTMX, and if HTMX can live up to those promises then great. For me it doesn't, currently, but it sounds like it does for others.
The relative link bugs are [0] and [1], and I fixed [2]. My fix was merged quickly, but the other two bugs of what appears to be similar significance, have been open for well over a year each. My issue however is less about these specific issues, and more the general lack of support for hypermedia, which speaks to a general state of HTMX not living up to its promises.
As for the DX, I think not having a build step, and code structure, are somewhat orthogonal. I agree with much of that essay, but most of it is about issues with complex build systems and the use of Typescript. It's not about having 190 top-level functions, which, with all due respect, I think indicates a lack of architecture. Again, the issue is less about the specifics, and more about the fact that HTMX is not living up to its promise of simplicity because of how impenetrable the codebase is in this regard.
As mentioned, so far I have found Stimulus and Turbo to be better implementations of what HTMX appears to promise to be. More activity in this space from anyone and everyone is great here though!
[0]: https://github.com/bigskysoftware/htmx/issues/1476 [1]: https://github.com/bigskysoftware/htmx/issues/1736 [2]: https://github.com/bigskysoftware/htmx/pull/1960
Why is the number of top level functions a problem? As long as they are named and organized appropriately I don't see the issue
I love writing prototypes as a single file as much as the next guy, but there's almost 5k lines of code in this one file: https://github.com/bigskysoftware/htmx/blob/master/src/htmx....
They're sectioned off, so "event/log support" and "AJAX" and such are grouped by the type of method, but they're not prefixed so there's no way for your editor to help you explore grouped functionality.
Given that the code isn't particularly organized or structured in any way that I could quickly glean (aside from the aforementioned grouping of related functionality, which is only so helpful), I think I'd be put off from wanting to contribute to this codebase.
De gustibus, but after taking a look personally I think it's pretty good. It's typed to the extent allowed by js, sections are clearly delineated, and if you collapse all the functions you can get a quick overview of every section. I personaly would use a different naming convention, but still their functions look reasonably named.
- [deleted]
See: php.
190 functions at a single level strongly implies that they're not named and organized appropriately.
Naming doesn't have anything to do with the number of functions. They section things off in the file, so if you prefer things split into multiple files I can understand, but it's a personal perference in something this size
If you're not using more common organization tools like modules, naming tends to be where organization is implemented: common prefixes, etc. Even if they're carefully done, you still end up with a long list of names, with a lot of implicit rules to keep in mind to decode the name (this is why Hungarian notation seemed like such a good idea in the beginning, and a bad idea once it was actually extensively used). Names shouldn't need decoding.
I haven't looked deeply at HTMX, so I won't claim they're falling prey to this exact problem. But it's definitely a code smell that's concerning.
i don't mind a lot of functions in a single file:
https://htmx.org/essays/codin-dirty/#i-prefer-to-minimize-cl...
The more you need to hold in your head at once to understand code the harder it is to do understand, and the harder it is to contribute or onboard to a codebase.
A lot of functions doesn't necessitate a lot of things to hold in your head, but in my experience, HTMX hasn't got enough other structure to prevent this. I was not confident about the changes I made, and the reviewer was not confident about the changes. In a well architected codebase the goal is for these things to be obvious.
As for minimising classes? Sure. I can get behind that. But I think it's orthogonal to having a lot of top level functions with no clear naming or sorting.
If the goal is to have a single-file codebase, I'd suggest considering the following (you may already have done so, but I haven't noticed consideration in the few documents I've read):
- Structuring the file into clearer regions – there is already some of this, but comment blocks are easy to miss in a 5.2k file, and utilities are everywhere.
- Adding named closures for grouping related functionality – "classes lite", at least gives some function namespacing and code folding.
- Ordering the file to help direct readers to the right bit, literate-programming style, so that there's a sort of narrative, which would help understand the architecture.
- Function name prefixes to indicate importance – is something the entrypoint to core functionality? is it a util that should be considered a sort of private function?
- Pure functions – so much of the code is state management performed in the DOM, which makes it hard to test, hard to know if it's working, hard to know what interactions will be introduced, etc. State management is always hard, but centralising state management more would be good.
- (That said... arguably library internals are the place to have make the low-abstraction high-performance trade-off with a bunch of mutable state. However, this makes it hard to have other Javascript that co-exists with HTMX, because it's too easy to stomp over each other's changes. A better integration path, like Stimulus, might alleviate this and retain HTMX's control over the DOM).
- I understand the preference for longer functions, but `handleAjaxResponse` is a lot. More abstraction would really help make this more understandable.
I get that personal preferences are key to why HTMX is the way it is, but I think it's important for the general health of open source projects that others are able to contribute, safely and effectively, and I'm not sure the current choices are most conducive to that. Hopefully some sort of middle ground can be found where HTMX doesn't lose its "personality"(?) but where some of these things can be improved.
Obviously it's an idiosyncratic way to organize code, and I get that someone coming in and making a one-time contribution might be a bit overwhelmed by how different it is than other codebases, but I've been happy with how easily i've been able to come into it after taking time off and figure things out, particularly when debugging.
We have other people contributing on an ongoing basis and i've had people (mostly non-JS people) comment on that they find the codebase easy to navigate, since you don't have to rely on IDE navigation.
There isn't a lot of state involved in htmx: it's mostly passed around as parameters or stored on nodes. History, where you ran into a problem, is a tricky part of the library that's hard to test, and that's where most of the problems have cropped up. I could probably factor that better to make it more testable, at the cost of more abstractions and indirection.
In general, I'm not inclined to change the organization much (or the codebase, per the article above) so that we can keep things stable (including the behavior of events, which apparently changed on you at one point.) I'll sand it, but I'm not going to do a big refactor. We've had people come in and propose big changes and reorganizations, and we've said no.
https://data-star.dev is a result of someone proposing a big htmx rewrite and then taking it and doing their own thing, which I think is a good thing. htmx will stay boring, just generalizing hypermedia controls.
I did a walk through the codebase here:
https://www.youtube.com/watch?v=javGxN-h9VQ
Given the lack of API changes going forward, I hope that artifacts like that, coupled with overall stability of the implementation, will mitigate risks for adopters.
I think an opinionated stance is in general a good thing when it comes to open source project. I just worry that contrarianism in frontend development is being conflated with contrarianism in general programming. The former being the intention of the project that I support (yay hypermedia!), and the latter most likely not being the goal.
Maybe the sanding will help, and if HTMX is not going to change much then maybe not much is needed, but I think there's still a way to go to stability and feature completeness.
> https://www.youtube.com/watch?v=javGxN-h9VQ
Thanks for the link, I'll definitely give this a watch!
i'm pretty contrarian in programming as well, so it tracks:
https://htmx.org/essays/codin-dirty/
htmx is feature complete (see the article)
we can argue about stability, but we have lots of happy users
> The relative link bugs are [0] and [1],
Thanks. Genuinely asking something here: are relative links actually common in frontend? I've only ever used absolute URLs for static assets. Actually I even made tooling that ensures that static asset paths will always be correct: https://yawaramin.github.io/dream-html/dream-html/Dream_html...
Of course, this kind of tooling exists for many frameworks. But I've never seen frontend frameworks suggest using relative links for static assets, they always seem to put them in a separate subdirectory tree.
Relative links are common in the Microsoft ecosystem; the IIS webserver, in its default configuration, serves websites at a subpath named something like /Our.App/ instead of at /. Frontends often use the <base> tag so that they can refer to assets by relative path, allowing different environments to have different base paths.
Random developer here: in my code, yes they are common. I tend to write modules so that I can route to them from wherever, reuse them, and move them around as I please. Relative links help greatly with such things.
Besides that, they are a very basic part of the spec, and I consider anything that breaks them to be truly fundamentally broken.
Understood, but how do you deal with static asset caching and cache-busting with version markers or similar? Does your framework/build system/whatever automatically add version markers to static assets that are bundled directly with your module?
I tend to keep statics consolidated and slap middleware on those routes to handle expiration headers and whatnot. Versioning is handled where links are generated; an href="{{ linkfor(asset) }}" style resulting in generated urls complete with content hash tacked on. I almost never construct asset links without some form of link generator function because said functions give me a centralized place to handle things just like this.
(edit: I should clarify that I'm mostly working in Go these days, and all my assets are embedded in the binary as part of compilation. That does make some of this much easier to deal with. Javascript-style build mechanisms tend far too much toward "complex" for my tastes.)
OK, wait, you said earlier that you bundle static content like images in the same module as the feature itself, but now you're saying you keep them consolidated in a separate place? You also said earlier that you commonly used relative links but now you're saying you generate them with a link generator function? I'm confused about what you're actually doing...if you look at the link I posted earlier, you can see how I'm doing it. So...are you using relative links like <img src=dog.png> or not?
I was unclear; I bundle them per-module; each module has an asset route with attached middleware. So for a users module, there will be users/assets or some such route, to which various static-related middleware gets attached.
The link gens inherently accept relative paths (foo) and output relative links with appropriate adjustments (foo.png?v=abcdef), though they can generate fully-qualified links if circumstances warrant (eg I often have a global shared css file for theming, though that's all I can think of offhand other than "/" for back-home links). The module can then be mounted under /bar/blort, or wherever else, and the links remain relative to wherever they are mounted.
On occasion I do hardcode links for whatever reason; my rule of thumb is relative links for same level or deeper, or root-anchored if you're referencing a global resource outside the current path, but there aren't many of those in my apps. A rare exception might be something like an icon library, but I tend toward font-based icons.
To put the rule more succinctly, I never use "../foo". Going up and then over in the hierarchy strikes me as a recipe for bugs, but that's just instinct.
I also never use fully-qualified (including hostname) urls to reference my own assets. That's just madness, though I vaguely recall that WordPress loves to do crap like that; there's a reason I don't use it anymore. :)
So the main difference if I understood your link correctly, would be that I would have maybe one or two items in /static/assets by root-anchored urls, and the rest accessed as /path/to/module/assets/whatever (edit: or just assets/whatever from within the module).
Because I'm doing this in go, the module's assets live where the code does and gets embedded in a reasonably standard way. I actually tend to use git commit as my cache buster to avoid having to hash the content, but full hashing can be done as well very easily just by changing the generator functions.
I can then just import the module into an app and call its associated Route() function to register everything, and it just works. Admittedly it's very rare that I reuse modules between apps, but it's easily possible.
For background to my thought process, I originally started using the link gen paradigm long ago to make it easy to switch CDN's on and off or move between them, back before whole-site proxying was considered best-practice. The rest is just a desire for modularity/encapsulation of code. I like to be able to find stuff without having to spelunk, and keeping assets together with code in the source tree helps. :)
Thanks for the explanation!
Sorry, forgot about this bit:
> and which event stopped working
It was `htmx:load`. On 1.x this fired on first page load, which I used to do a bunch of setup of the other little bits of JS I needed, and which would cause that JS to re-setup whenever the page changed.
On 2.x this never fired as far as I could tell. Maybe I got something else wrong, but only downgrading to 1.x seemed to fix it immediately, I didn't investigate further.
I did wonder if there were breaking changes in 2.x, but as far as I could tell from the release notes and documentation there were not.
Maybe I'm missing something here, but JS modules do not require a build step.
* Note to non-JS hackers: JS module symbol scope is per-source file.
JS modules can't be imported with a plain script tag.
Script type=module doesn't work?
Not if you want to support the 0.2% market share that IE 11 has.
That's true, but also a trade-off that is a perfectly valid engineering choice for many or even most teams.
HTMX 2 stopped IE support, so that shouldn't be an issue.
we don't :)
we want to support the traditional script tag w/ a src attribute and nothing else
Why?
idk just like it better that way
Chiming in to say modules are awesome and you shouldn't be so scared.
I'm not scared, I just want people to be able to include htmx on their page the same way, for example, good ol'jQuery is included.
We generate esm and cjs modules for people who want to use htmx in that manner though:
you can define import maps in a separate <script> tag and reuse the module name elsewhere
sure, but I wanted the same experience of, say, jquery, where you just drop in a script tag w/ a src attribute and it just works
an aesthetic decision, I suppose
Depending on the import tree depth, it significantly increases latency.
(Which is why bundlers still exist.)
Last time i tried it, i couldn't get hotwire to work with a non-ruby backend.
HTMX had no issue, tho
It’s definitely documented as Rails first, but so far I’ve had no compatibility issues using it with my Swift backend.
I’m curious about your Swift backend stack, if you’re willing to describe it. I’ve become quite fond of the language after writing a few small desktop apps & command-line tools with Swift.
I love the language. I'm using Vapor+Fluent+Leaf, but I've had a few issues with them, I think they're fine, but if I was making decisions again I'd probably choose something else. I think the problem is that Vapor is trying to be a Django/Rails style, batteries-included framework as opposed to a Flask-style library (if those terms help you at all), but they've not quite nailed it to the Django/Rails level yet. For my project I'd have been better off with less framework, as most of my code is not strictly web code. I've ended up changing the Vapor application architecture a bit and pulling lots out into non-web-specific Swift Modules. If I were choosing again I'd give consideration to Hummingbird.
Library availability has mostly not been a problem for me, there are some really high quality libraries, particularly open source ones from Apple. Quality is high compared to other ecosystems.
I'm targeting Linux/Docker for deployment and while I don't have the project running in production yet, I've been running CI builds and it so far seems fine. It's fairly clear when things are Linux compatible and when they're not. Most things you'd expect to be are compatible. Some third-party libraries are accidentally not Linux compatible, but it hasn't been much of a problem for me.
The new Swift Testing library is really nice, and I'm using that for all my server tests with no real problems. Vapor could have more testing support (Django's is excellent and my bar for comparison), but it all works.
Overall it's just a lovely language to work in, and I'm enjoying server side development with it.
Thank you, gives me some ideas on what to try, it's appreciated!
no doubt there was something small that i was missing, but i was doing a rapid, time-boxed experiment and didn't have time to get deeper into it, at the time
I use it at work with asp.net MVC, no issues either. I did have to use Webpacker though...
Symfony (PHP) and it's UX packages are all based on Stimulus
I have had a great experience with Astro and HTMX. When the first time I tried, I didn't think much about HTMX and thought it was an Astro thing, about a year or over. But this time, I had a great experience and I understand the power of htmx and native web only JS, no framework when building simple sites is much refreshing.
I wanted to use Hotwire but it felt like it was made for Rails as backend? This probably needs to be fixed because if the DX is as good as you say then its disservice to be tied to Rails.
Which leads to the appeal of HTMX. It promises no other dependence. It just does exactly what it advertised. I'm not sure whether your issues were due to implementation or specific known bugs in HTMX not covered.
I am curious to know more about Hotwire, I'm just worried about state management and this is greatly overlooked when it comes to adding application logic beyond just hydration of specific components on page.
So far I have not seen anything that competes with Sveltekit.
Hotwire is designed for good integration with Rails, but Rails is not required. In a similar way, HTMX works better with various server-side libraries, but none are required.
Personally I'm using Hotwire (was using HTMX) with a Swift backend with no issues.
- [deleted]
Just a note to other readers, Turbo doesn't require Stimulus... I use Turbo mainly with AlpineJS and it works just as well since Turbo is the just tag driven side of things.
> It’s a single 5k line file with 190 top level functions in it
That's interesting. Do some people prefer this style? If yes, how come?
I do. I've always felt that I'm doing something wrong when I do this, but I much prefer not having to switch files all the time and navigate folders etc. looking for things. With everything in one file, I can just use regular within-file search (and M-x occur in emacs which is a bit like clickable (and editable) grep). If I need "focus", I just use narrowing in Emacs (shows only the highlighted part of the buffer, with handy shortcuts for narrowing to current function/class). If I need to see something side-by-side, just split or clone the buffer. I loathe working on those java etc. projects where you have a zillion classes each in their own file, many with maybe a handful of members each; whenever I switch files I feel like I'm losing context.
I prefer this style. I primarily use Emacs as someone else mentioned. I feel like it's sort of similar to editor tabs vs buffers. Like, once you have a good way of switching between buffers, separate editor tabs seem like a superfluous UI element. In a similar way, if you have powerful ways of navigating to the function definition you want, then everything being in the same file doesn't seem to matter.
Also, having all the code in a single file helps expression exposition of the code. You can sort of captivate the reader of your code, and present the code in the order of your choosing.
Really appreciate the reply. Thanks.
If you look at the end-user API documentation[1] it's pretty good.
But the htmx.js file itself seems to struggle from a certain type of internal naming scheme that will take a while to get up to speed with and doesn't really lend itself to being easy to context switch in & out of it, where you might simply forget that things exist and end up re-writing or inlining these fragments.
This naturally stems from a very organic development process, where something starts out as a quick prototype, then the functions get too big so you split fragments out into things like `shouldProcessHxOn` and `processHXOnRoot` which absolutely make sense at the time where you're avoiding duplication, and trying to turn it into something 'more proper' with 'better abstractions' would actually cause the code to balloon in size and make it much bigger and much more like object spaghetti.
On the whole though, a large amount of the file is type documentation, e.g. `/* @param {Node} start */` which is more a failing of JavaScript itself than anything else, and honestly a 5k self-contained file with good public documentation of what's user-facing vs internal is pretty easy to grok if it's something you work with frequently.
If you were to rewrite this in a saner language like Python with a more functional style, you could probably condense it down to 1000 lines or less.
Backend dev here. Give yourself a break and use sveltekit
Fun fact: Svelte doesn't allow the <title> tag to have any attributes: https://github.com/sveltejs/svelte/issues/5198
Full stack polyglot dev here, the relentless pressure to use Sveltekit that has utterly destroyed plain Svelte docs, IMO, coupled with the koolaid kult around v5 is why i finally ditched svelte.
I have backend solutions and existing databases and existing backends. I would never consider a Js backend in a production app to begin with, despite the benefits an isomorphic approach offers. I dislike when physical actual boundaries in code execution contexts are obfuscated, for one...
But not everyone is building SPAs, and my dev server is my actual backend, thanks
Remember Meteor?!
Yeah, Svelte5 said "away with these pesky svelte users, lets market to React devs and make everyone realize that Vue3 has a bigger ecosystem, since you are gonna have to do a rewrite anyway "
Basically the opposite of this principled stance here on this htmx missive
Frontend framework churn costs money and lives
It doesn't have to, you can just start using a piece of technology 5-10 years after everybody else, you know.
We started using react a couple of years after hooks were introduced when there were millions of ready libraries, all major design patterns were established, tons of docs were written, etc., and although there's still major churn, it's nothing like it was when react first came out.
I looked at svelte when the hype circle started and decided to postpone using it for at least five years — these comments show it was a right decision.
It saves money and time to be conservative with these things.