Understanding Htmx + Hono
By
What I learnt from looking beyond Next.js
My journey as a developer started a few years ago, before AI coding tools became commonplace. I watched a few videos about JavaScript, HTML and CSS, then coded my first "Hello World!" website.
I eventually learnt React and Next.js, and Next.js soon became my default whenever I had to build a web application.
During my internship at GovTech, I learnt that it might not always be the best fit for a simple website. Sometimes, it introduces more moving parts than the problem needs, and more dependencies to maintain and patch.
That sent me back to some basics. Here, I try to document what I've learnt.
Understanding web requests
When we open a website, our browser sends a request to a server. The server returns a response.
If that response contains HTML, the browser can use it to display a page. It doesn't need to know which framework produced it.
For example, when we request `/menu`, the server might return:
- HTML generated ahead of time, perhaps during a build. This is commonly called **Static Site Generation (SSG)**.
- HTML generated when the request arrives, perhaps using information from a database. This is **Server-Side Rendering (SSR)**.
Either way, the browser receives HTML. The difference is when that HTML was produced, which affects things like freshness, caching and response time.
There's another possibility too: the server sends a minimal HTML page and some JavaScript, then the browser runs that JavaScript to build the interface. That's **client-side rendering**.
These approaches can also be combined. A website doesn't have to pick just one for everything.
What plain HTML already gives us
Without writing any JavaScript, we can already use links to navigate and forms to submit information.
By default, these interactions usually load a new page. That's perfectly reasonable for plenty of websites.
But imagine clicking "add to cart" and having the entire page reload, even though most of it hasn't changed. The header is the same. The product list is the same. Only the cart needs updating.
A full navigation can also interrupt what you're doing or move you away from your place on the page. Annoying, right?
What we'd really like is to update just the relevant part.
How the browser builds what we see
The browser reads HTML and builds a tree of objects called the **Document Object Model (DOM)**. Together with CSS and the browser's layout and painting processes, that tree determines what appears on screen.
JavaScript can change those objects directly.
For example, it can update the text inside a cart counter without loading a new page. It isn't editing the original HTML file on the server. It's changing the browser's current document.
That distinction matters when we refresh.
If a change exists only in the current page's DOM or JavaScript memory, a refresh normally loses it. To make it survive, we need to save it somewhere, such as browser storage or the server.
Changing what's on screen doesn't automatically change what's saved.
The problem of keeping things in sync
An interactive page often has two things to keep in agreement: its state and how that state is displayed.
Say my cart contains three items. That information might affect several parts of the page:
- A counter in the header.
- The cart's item list.
- A running total.
- A message that appears when the cart is empty.
With manually written DOM updates, I have to make sure every relevant display changes whenever the cart changes.
Adding an item? Update the displays.
Removing an item? Update them again.
Clearing the cart? Don't forget the empty-cart message.
I can organise that code into shared functions, but I'm still responsible for wiring everything together.
And if I miss an update, the page might not crash. It might quietly show the wrong thing.
That's one of the problems React helps solve.
What React does
Instead of writing every instruction for changing the page, React lets me describe what the interface should look like for a given state.
For example:
```jsx
function CartSummary({ count }) {
return (
<div>
<p>Cart: {count}</p>
{count === 0 && <p>Your cart is empty.</p>}
</div>
);
}
```
This describes the result I want. If `count` is zero, show the empty-cart message. Otherwise, leave it out.
When a relevant state update triggers a render, React calls components to calculate the next interface. It then commits the necessary changes to the DOM.
The bit I misunderstood was how React knew what to update. I imagined it searching my code for every mention of `count`.
It doesn't do that. It works from the output of rendering the components.
I still have to manage state correctly, but I no longer have to manually update every DOM element that displays that state. That removes a whole category of easy-to-miss mistakes.
What building more of the page in the browser costs
There's an important distinction here: **React doesn't require the whole page to be built in the browser**. React can render HTML on the server, and frameworks such as Next.js support that.
Still, when I choose an approach where the browser does most of the rendering and data fetching, there are costs to consider.
JavaScript has to arrive before the interface works
In a purely client-rendered application, the initial HTML may contain little more than a root element.
The browser has to download and run the JavaScript before the main interface appears. On a slow connection, that can mean an empty page or loading screen while the user waits.
Server rendering can improve this, though interactive features may still need JavaScript.
Data needs to become an interface
A server might return something like this:
```json
{ "count": 3 }
```
The browser then needs code that understands the response and turns it into the right interface.
That means agreeing on a data format, retrieving the data and handling it on the client. This separation can be useful, especially when several clients share an API, but it's still work to build and maintain.
### Requests need loading and failure behaviour
While data is being fetched, what should the user see?
If the request fails, what happens next?
These questions aren't unique to React. Any interface making background requests has to answer them. But spreading data fetching across many components can create more places where I need to think about them.
### Server data can become stale in the browser
The server might hold the saved cart while the browser keeps a local representation of it.
If the cart changes elsewhere, that local representation can become outdated.
Now I need to decide when to fetch again, how to handle updates and what to do if requests finish in an unexpected order.
Libraries can help, but the underlying coordination problem still exists.
None of this makes client-side rendering a bad choice. It just means I should know what I'm buying before I pay for it.
## Asking a different question
React helps keep application state and the displayed interface in sync.
But what if a simple website doesn't need much application state in the browser in the first place?
Suppose the server already knows how many items are in the cart. Instead of sending data and asking my client code to build the display, it could send the display itself:
```html
<header id="cart-header">Cart: 3</header>
```
The browser already understands HTML.
There's still a representation of the cart on screen, and it can still become stale. Sending HTML doesn't magically remove that problem.
What it can remove is the need for a separate client-side cart model and the rendering code that turns that model into markup.
For some websites, that's a useful simplification.
## Where htmx comes in
htmx lets HTML elements make background requests and use the returned HTML to update part of a page.
For example:
```html
<header id="cart-header">Cart: 3</header>
<button
hx-get="/cart/header"
hx-target="#cart-header"
hx-swap="outerHTML"
>
Refresh cart count
</button>
```
Read it quite literally:
1. When the button is clicked, request `/cart/header`.
2. Receive an HTML response.
3. Replace the existing cart header with that HTML.
The server returns something like:
```html
<header id="cart-header">Cart: 4</header>
```
The rest of the page stays in place.
For this interaction, I don't need a client-side component to interpret `{ "count": 4 }`. The server has already produced the markup.
I still need to handle slow requests, failures and any other parts of the page affected by the change. htmx simplifies how the update happens; it doesn't remove every problem around it.
### What happens without JavaScript?
htmx itself is JavaScript, so its enhancements won't work when JavaScript is disabled.
However, I can build on ordinary links and forms that already work, then use htmx to improve the experience. This is called **progressive enhancement**.
For example, a form can submit normally without JavaScript and update just one region when htmx is available. The server needs to support both paths, returning a full page for ordinary navigation and a fragment where appropriate.
That fallback is something I design deliberately. Adding an `hx-get` attribute to a button doesn't automatically provide it.
Similarly, a React site isn't necessarily blank without JavaScript. It depends on how the site is rendered and which interactions require client-side code.
## Where Hono fits
In this setup, Hono handles the server side.
It routes incoming requests to the appropriate handler, which can read data, process a form and return a response.
It also supports middleware for shared concerns such as logging, authentication and security headers.
What caught my attention was that I could still write markup using JSX:
```jsx
function Header({ count }) {
return <header id="cart-header">Cart: {count}</header>;
}
```
Using Hono's server-side JSX rendering, this can become HTML sent straight to the browser:
```html
<header id="cart-header">Cart: 3</header>
```
The component doesn't need to run in the browser for that to happen.
That cleared up another misunderstanding for me: **JSX is not React**.
JSX is a syntax for writing markup-like expressions inside JavaScript. What those expressions do depends on the tooling and runtime using them.
Here, I'm using Hono to render HTML on the server. React doesn't need to be in the project at all.
## Where htmx gets less straightforward
I don't want to replace one default with another without looking at the trade-offs.
### Updating several parts of the page
A single target is easy to reason about.
But what if adding an item needs to update the header count, the cart total and a checkout button in three different places?
htmx supports this, including through out-of-band swaps, which let one response update additional regions. I could also replace a larger shared section.
The work is deciding which regions need updating and making sure they stay consistent.
React often feels more natural when many parts of an interface depend on shared client state. That's exactly the kind of coordination its rendering model helps with.
### Interactions that need immediate local feedback
Dragging a seat around a floor plan needs updates as the pointer moves.
Sending a server request for every movement wouldn't provide the experience I want. The browser needs to handle that interaction locally.
That doesn't necessarily require React, but it does call for client-side logic. For a complex interactive interface, React may be a very sensible choice.
### What the team can maintain
The technology isn't the only consideration.
If the people maintaining the application already know React well, choosing a different approach has a learning cost.
I need to consider who will debug it, extend it and take over after I leave. A simpler architecture only helps if the team can work with it comfortably.
## What would I reach for now?
The mistake wasn't learning Next.js or choosing it for previous projects.
It was choosing it automatically, without asking which parts of it the project needed.
### A mostly informational website
I'd start by considering static HTML or a static site generator.
If the content can be generated ahead of time and there's little interaction, I might not need a running application server or much client-side JavaScript at all.
### A seven-step form
I'd consider Hono and htmx if most of the work involves submitting information, validating it on the server and displaying the next step.
Server-side validation is necessary regardless, because client input can't be trusted. Returning HTML can keep validation and its presentation close together.
The trade-off is that server-driven steps depend on network round trips. I'd also need to handle focus, error messages, back navigation and preserving entered information carefully.
Accessibility would still be something I build and test. No framework gives it to me automatically.
### A collaborative seating-plan editor
I'd lean towards React.
Dragging seats, selecting several items and giving immediate visual feedback all benefit from local state and client-side rendering.
I'd accept the extra client-side complexity because it serves a clear purpose.
Live collaboration would still be a separate problem to solve, including how changes reach other users and what happens when edits conflict.
### An internal dashboard
I'd first ask how people actually use it.
If filtering means submitting a selection and receiving an updated table, Hono and htmx could work nicely.
If users need to explore already-loaded data instantly, coordinate several charts or make lots of local edits, React becomes more attractive.
The word "dashboard" alone doesn't decide the architecture. The interactions do.
## Choosing with a reason
Every architecture comes with a bill. Sometimes it's client-side state and data fetching. Sometimes it's more server round trips or more explicit coordination between page regions.
The lesson I'm taking from my internship is to understand that bill before choosing the tool.
I'll still use React and Next.js. I just want a better reason than "that's what I always use."
Sometimes the website needs a rich application running in the browser.
Sometimes it just needs the server to send back some HTML.