Building my first htmx application
After updating docket to use htmx, I wanted to share my experience. First and most important, HTMX is more than just a client side framework. Instead of using a JavaScript library to render your application in the browser, you have incrementally load HTML as your application’s state changes. Loading behavior is defined in special HTML attributes instead of code. I’ve spent the last few years building RESTful data APIs with client side rendering. Going back to server rendered HTML was actually quite refreshing, and being able to drive both the client and server behavior from a single set of HTML templates was fantastic.
The client rendered model that many applications have been built comes with some drawbacks that you aren’t exposed to with a htmx application:
- You spend a lot more time building your application. In a client rendered applications you need to define APIs, build and test those APIs. Alongside that, you need to build a frontend that consumes those APIs. Client rendered applications often are more complex, and require deep knowledge in multiple technology stacks.
- Single page applications consume more resources. Getting all the JavaScript you’ve created to users isn’t free for you or the user. Bigger applications mean bigger payloads, more bandwidth, and poor performance on slower devices.
- Ecosystem churn will eventually catch up with you. This can manifest as needing to keep dependencies up to date to patch security issues. Or spending time refactoring your application so that you can upgrade to the next release. You will likely also need to replace or take over maintenance of some of your dependencies as packages are abandoned.
The latest generations of JavaScript frameworks are starting to lean towards server side rendering, which I find entertaining. Rendering HTML on the server is how we built applications before client side rendering came into fashion. I recently came across htmx. The premise of htmx is that if we’re going to build interactive HTML driven applications, we should use HTML and hypermedia to drive our applications. Instead of transferring state around as JSON and converting it back into HTML with JavaScript we could use an expressive & declarative attributes enhancing the hypermedia functionality in HTML.
HTMX enhances HTML by enabling any element to trigger HTTP requests. Not only can any element trigger requests, you can also trigger HTTP requests with other events (even custom ones), and use all of the HTTP methods that we’ve been exposed to through RESTful APIs. A simple example from the htmx.org website:
- <script src="https://unpkg.com/htmx.org@1.9.10"></script>
- <!-- have a button POST a click via AJAX -->
- <button hx-post="/clicked" hx-swap="outerHTML">
- Click Me
- </button>
When a user clicks on this button, an AJAX request is sent to /clicked
, and the contents of that response will replace the outerHTML
of the button. The important part is that your server responds with HTML, and that HTML is patched into the current DOM.
A shift in thinking
With client rendered applications, one needs to build robust full featured data APIs, and use that data to incrementally build the interface. As your UI changes state, you may need to go back to the server and get more data, so you can render the new state. With an HTMX app, each incremental UI state is loaded from the server as HTML. Each link that is clicked, and each form submission, gives you an opportunity to send data to the server and get updated HTML.
A great example of this is building chained select elements. When a selection in one select element changes the options in another select box. The classic case of this is a car manufacturer, and model selection interface. The htmx example page has a great demo of this:
- <div>
- <label >Make</label>
- <select name="make" hx-get="/models" hx-target="#models" hx-indicator=".htmx-indicator">
- <option value="audi">Audi</option>
- <option value="toyota">Toyota</option>
- <option value="bmw">BMW</option>
- </select>
- </div>
- <div>
- <label>Model</label>
- <select id="models" name="model">
- <option value="a1">A1</option>
- ...
- </select>
- </div>
When a request is made to the /models
endpoint, the response will be a fragment of HTML:
- <option value='325i'>325i</option>
- <option value='325ix'>325ix</option>
- <option value='X5'>X5</option>
This HTML is spliced into the DOM, replacing the content of <select id="models">
element that was indicated with hx-target
attribute. With only a few attributes we’re able to create a complex interaction without writing any JavaScript. Instead of building APIs and views separately, we build endpoints that render HTML. Because everything is ‘just HTML’, debugging is simpler as you can lean on your server side framework more and not have to context switch as frequently. If you have other clients that need a JSON data API, you can build that separately. This allows you to separate the concerns of your browser application and data APIs.
How my conversion to htmx went
While converting my react to PHP & HTML templates was tedious it was relatively easy. React components were frequently converted into HTML template partials. I re-used the bulk of my stylesheets so that the application looked the same whether a page was rendered in react or htmx. This allowed me to incrementally rebuild my application. I started off with the simplest views and worked my way up to the more complex views. Before adding htmx in to the application, I wanted to relearn how much could be done with just HTML links and forms. This allowed me to get many of the ‘basic’ views and forms working with just HTML. There were more than a few rough corners where the UX offered by browsers wasn’t working for me. An example of this is selectboxes that have richly formatted items, and typeahead filtering. To get this UX working, I needed to lean on webcomponents in addition to htmx
. Pairing htmx
and webcomponents
allowed me get the rich UX that I had with react but with far less JavaScript to maintain.
Getting fast navigation was where I started adding htmx
. With a few attributes you can turbo-charge your application’s navigation. Using hx-boost
on links, will replace the native navigation with an AJAX request that will swap the body
element instead of replacing the whole document. When making boosted requests, htmx
will set a Hx-Request
header that you can use to render only the necessary HTML on the server yielding improved performance. I used hx-get
and friends quite a bit to make dynamic operations like data aware context menus, modal windows and more. I found that combining the incremental fetching of htmx
with web components to be wonderful. For functionality I couldn’t download from the server, I covered with webcomponents. Webcomponents allow you to define new HTML elements with their own attributes, and behaviors. A very simple webcomponent would look like:
- class SimpleLogger extends HTMLElement {
- constructor() {
- console.log(this.getAttribute('message'));
- }
- }
- window.customElements.define('simple-logger', SimpleLogger);
This component can be used in our HTML with:
- <div>
- <simple-logger message="hello world"></simple-logger>
- </div>
This is obviously a trivial example, but with webcomponents, I’ve been able to build all the custom UX I used to need react for. Custom elements have a couple requirements. Names have to be lowercase, have -
in them. Webcomponents must use closing tags. While the webcomponents API isn’t ideal, it is functional, and there are libraries that improve ergonomics. htmx
and webcomponents also pair perfectly with the growing ecosystem of ‘vanilla js’ libraries. For my project, SortableJS was incredibly useful.
Build tooling for htmx
While you don’t need to have a build system to use htmx. My application, was already using vite
to build TypeScript, and saas into JavaScript CSS respectively. I wanted to maintain these toolchains as they are productive for me. With react out of the picture, my vite
configuration now looks like:
- import {defineConfig} from 'vite';
- import path from 'path';
- const projectRootDir = path.resolve(__dirname);
- export default defineConfig({
- plugins: [],
- build: {
- emptyOutDir: false,
- outDir: './webroot/',
- // make a manifest and source maps.
- manifest: true,
- sourcemap: true,
- rollupOptions: {
- // Use a custom non-html entry point
- input: path.resolve(projectRootDir, './assets/js/app.tsx'),
- },
- },
- resolve: {
- alias: [
- {
- find: 'app',
- replacement: path.resolve(projectRootDir, './assets/js'),
- },
- ],
- },
- esbuild: {},
- optimizeDeps: {
- include: [],
- },
- server: {
- watch: {
- ignored: ['**/vendor/**'],
- },
- },
- });
This build configuration lets me build my project with TypeScript, and bundle htmx
, my webcomponents and utility functions into a minified assets.
I’ve really enjoyed my first project with htmx
, it has been refreshing to build a dynamic, interactive application with a handful of dependencies and only 57kb of JavaScript for the entire project. I will definitely be using htmx
again in future projects. I don’t know if htmx
would scale up to a multi-player application or collaborative document editing, but for many content workflow applications I think htmx
, webcomponents
, and server rendered HTML are a stack that you should try.
Great read and a good lesson for anyone who is to caught up in React or Vue for websites that are too simple for React’s and Vue’s niche use-cases.
Sven on 2/9/24
Thanks for this, HTMX looks pretty interesting and makes me less leery of frontend!
You should become the CEO of HTMX.
Chris Sheppard on 3/27/24
Corporis in id qui
Guinevere Cervantes 4 days ago