HTMX first class support on deco.cx

Unleash the power of native web APIs
05/27/2024·Tiago Gimenes

We are thrilled to announce a new HTMX App on deco hub. HTMX has been gaining popularity for its ability to create dynamic web applications without relying heavily on JavaScript. By leveraging HTML attributes, HTMX simplifies the development process, making it easier to build interactive user interfaces.

Why support HTMX now?

At deco.cx, we intend to support all major frameworks, including Angular, Next.JS, SvelteKit, and more. However, we started with HTMX because we see a significant potential in simplifying both the development and production of simple web pages. HTMX shines in scenarios where developers want to avoid the complexities of build steps, hot module replacement (HMR), and other tooling. With HTMX, what you write in HTML is exactly what you see in both development and production environments.

By combining HTMX with the strengths of HTML5 and CSS3, developers can deliver good user experiences and top-class performance without the overhead of downloading, parsing, and compiling heavy JavaScript libraries. Additionally, using HTMX can result in more robust web applications. Testing JavaScript across all devices and browsers (Safari, Samsung browser, etc.) is time-consuming and expensive. Having your UI rendered on the server saves you countless hours of debugging across different environments, ultimately reducing Sentry bills.

How deco.cx Integrates with HTMX

If HTMX offers so many advantages, why isn't it the standard for web application development? The primary challenge with HTMX lies in the need to create routes for each UI state, which can complicate the development process. Each interactive element or dynamic update often requires a corresponding server route, leading to a proliferation of endpoints and increased maintenance complexity.

Moreover, the web is mostly comprised of centralized servers, and computing new UI states on a centralized server can penalize peripheral users due to latency issues. This is not the case with deco.cx, where our edge-first infrastructure distributes the code globally. This ensures that computing new UI states is much cheaper and faster, thanks to our low latency and globally distributed infrastructure.

For us at deco.cx, the most difficult part of using HTMX is having to create a route for each UI state. To address this challenge, we developed a new hook called useSection. This hook automatically creates routes for rendering your UI states without requiring developers to handle routing manually.

To demonstrate the power and simplicity of the useSection hook, let's explore an example by building a counter component.

Building a Counter with useSection on deco.cx

In this guide, we'll build a simple counter component using HTMX and the useSection hook on deco.cx.

preact

Preact Version

First, let's look at the usual Preact code for this component using the useState hook:

import { useState } from "preact/hooks";

export default function Section() {
const [count, setCount] = useState(0);

return (
<div class="container h-screen flex items-center justify-center gap-4">
<button
class="btn btn-sm btn-circle btn-outline no-animation"
onClick={() => setCount(count - 1)}
>
<span>-</span>
</button>
<span>{count}</span>
<button
class="btn btn-sm btn-circle btn-outline no-animation"
onClick={() => setCount(count + 1)}
>
<span>+</span>
</button>
</div>
);
}

Refactoring to HTMX

To refactor this component into HTMX, we follow three simple rules:

  1. Client-side hooks like useState and useEffect are removed.
  2. All variables started by a useState hook are placed on the component's props.
  3. Client-side event handlers like onClick and onChange are removed

By applying rules 1 and 2 to the Section component, we move the count variable to the component's props, leaving us with:

export default function Section({ count }: { count: number }) {
return (
<div class="container h-screen flex items-center justify-center gap-4">
<button
class="btn btn-sm btn-circle btn-outline no-animation"
onClick={() => setCount(count - 1)}
>
<span>-</span>
</button>
<span>{count}</span>
<button
class="btn btn-sm btn-circle btn-outline no-animation"
onClick={() => setCount(count + 1)}
>
<span>+</span>
</button>
</div>
);
}

Notice we don't need the import of useState anymore. To apply rule number 3, we need to remove the onClick handler, however, how can we keep the interactivity? That’s where the useSection hook is handy.

Integrating useSection

To implement the onClick functionality, start by importing useSection from deco/hooks/useSection.ts. This hook lets you create a link for the current section instance and override any props this section receives. By refactoring this component, we get:

import { useSection } from "deco/hooks/useSection.ts";

export default function Section({ count = 0 }:{ count: number }) {
return (
<div class="container h-screen flex items-center justify-center gap-4">
<button
hx-get={useSection({ props: { count: count - 1 } })}
hx-target="closest section"
hx-swap="outerHTML"
class="btn btn-sm btn-circle btn-outline no-animation"
>
<span>-</span>
</button>
<span>{count}</span>
<button
hx-get={useSection({ props: { count: count + 1 } })}
hx-target="closest section"
hx-swap="outerHTML"
class="btn btn-sm btn-circle btn-outline no-animation"
>
<span>+</span>
</button>
</div>
);
}

Explanation

Let’s dissect each part starting with the hx-get attribute. useSection({ props: { count: count - 1 }}) creates a link for the current section instance, but overrides the count prop. This means that if we had other props, the new count value would be merged with the other prop values, and a link for this section would be returned. This is beneficial as the developer can now override some props inserted by the deco.cx CMS.

The hx-target and hx-swap combo is useful when you want to replace the whole section at deco.cx, as sections are rendered under a <section/> element.

htmx

Handling Slow Connections

For 3G connections, where performing a request to increase/decrease the counter can be slow, we need to add a loading state. HTMX provides the htmx-request indicator class for this purpose. When HTMX is performing a request, the htmx-request class is added to the DOM. With TailwindCSS v3, we can create specific rules activated once this class is present. Here's the updated button:

<button
hx-target="closest section"
hx-swap="outerHTML"
hx-get={useSection({ props: { count: count - 1 } })}
class="btn btn-sm btn-circle btn-outline no-animation"
>
<span class="inline [.htmx-request_&]:hidden">-</span>
<span class="hidden [.htmx-request_&]:inline loading loading-spinner" />
</button>

The magic is in the [.htmx-request_&]: selector. This tells Tailwind to create a selector that is activated whenever the htmx-request class is present on a parent element. When performing a request, the CSS will hide the - element and display a spinner. This approach leverages native Web APIs to enhance user experience even on slow connections. Here's the final result:

htmx+loading

Warnings

When using the useSection hook, keep in mind that the props passed to the hook go into the final URL. Be cautious of URL size limits and try to pass small payloads like booleans and IDs.

Conclusion

We have a useSection API reference available for those who want to dive deeper. Additionally, we provide a recipe for migrating from Preact to HTMX and vice versa, detailing common design patterns and how to achieve them with HTML5 and CSS3. With these tools, developers can leverage the power of HTMX and the flexibility of deco.cx to build robust, high-performance web applications.

For more information, visit our docs: https://deco.cx/docs/en/api-reference/use-section.