A few new tricks have shipped with the .NET 8 release, and I’d like to take this time to experiment with them. Specifically, I wanted to see if folks investing in a Blazor component library could still use the excellent HTMX library. If you want to write even less JavaScript, this blog post will be right up your alley.

This post will explore how to take a server-rendered component and give it some client-side flair without needing web sockets or web assembly. We’ll even explore rewriting the Counter component found in the Blazor template and building it with HTMX in mind. Let’s go!

What is HTMX?

For folks familiar with Blazor’s interactive server mode, SignalR, aka Web sockets, the HTMX model isn’t much different.

The client communicates with the server, and the server retains stateful information. The big difference is that HTMX takes a hypermedia approach, meaning it leans on the web’s traditional request/response nature. There are no persistent connections between the client and the server. Any DOM element can initiate a request, wait for the server to process it, and then respond with appropriate HTML. Once the HTML payload is received, it is swapped into the current DOM. While the concept is simple to understand, it is powerful in practice.

What are Blazor Server-Rendered Components?

With the .NET 8 release, folks can opt-in to multiple render modes. The first in the list of render modes is Static, although that’s not entirely accurate.

Static rendering implies you could compile components and assets into HTML at build time. In the case of Blazor, “ Static” rendering is more comparable to its contemporary approaches of MVC and Razor Pages. When a request to a page or component is made, the server renders the component and its component graph and then responds with HTML.

These Blazor components are all HTML, meaning they can only use HTML features and not the same interactive model you may expect from Interactive Server or Interactive WebAssembly modes.

The advantage to these Components is they are lightweight payloads, fast to render, and can even be streamed via stream rendering.

Let’s Use HTMX with Blazor

Before we port our Counter component to use HTMX, we must set up our project to make Blazor play nicely with HTMX.

The first step is to add HTMX to our App.razor file. This is the app shell file which has our HTML structure.

<!DOCTYPE html>  
<html lang="en">  
  
<head>  
    <meta charset="utf-8"/>  
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>  
    <base href="/"/>  
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>  
    <link rel="stylesheet" href="app.css"/>  
    <link rel="stylesheet" href="BlazorHtmx.styles.css"/>  
    <link rel="icon" type="image/png" href="favicon.png"/>  
  
    <script defer src="_framework/blazor.web.js"></script>  
    <script defer src="https://unpkg.com/htmx.org@1.9.8"></script>  
    <script defer src="js/htmx-blazor.js"></script>  
    <HeadOutlet/>  
</head>  
<body>  
<Routes/>  
</body>  
</html>

We’ll also need to write a bit of JavaScript to tie into Blazor’s enhanced rendering mode.

// An enhanced load allows users to navigate between different pages  
Blazor.addEventListener("enhancedload", function () {  
    // HTMX need to reprocess any htmx tags because of enhanced loading  
    htmx.process(document.body);  
});

Now, let’s set up our HtmxCounter component. In a file, add the following Blazor component code. You’ll notice that HTMX uses hx-* attributes to define the behavior of DOM elements. It’s easy to pick up and can add functionality quickly. (Not to brag, but I got this sample working on the first try. 🤩)

<div class="counter">
    <p role="status">Current count: @State.Value</p>
    <button class="btn btn-primary"
            hx-post="/count"
            hx-target="closest .counter"
            hx-swap="outerHTML">
        Click me
    </button>
</div>

@code {
    [Parameter, EditorRequired] 
    public HtmxCounterState State { get; set; } = new();
    
    public class HtmxCounterState
    {
        public int Value { get; set; } = 0;
    }
}

Next, let’s look at our endpoint holding on to state. Note that this use of state management is only for demo purposes. I recommend user-scoped state management like a database limited to a single user.

In your Blazor’s Program.cs file, you’ll need to register the state for our component.

builder.Services
    .AddSingleton<HtmxCounter.HtmxCounterState>();

Next, we’ll need the endpoint to increment and render our component HTML fragment.

app.MapPost("/count",
    (HtmxCounter.HtmxCounterState value) =>
    {
        value.Value++;
        return new RazorComponentResult<HtmxCounter>(
            new { State = value }
        );
    });

Finally, let’s add our component to a Blazor server-rendered page.

@page "/"
@inject HtmxCounter.HtmxCounterState CounterState

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<div class="mb-4">
    <HtmxCounter State="CounterState"/>
</div>

@code {
    protected override void OnInitialized()
    {
        // reset counter on page reloads
        CounterState.Value = 0;
    }
}

Let’s see what happens when we load our page.

That’s pretty cool. From a client perspective, you can’t tell which implementation uses WebSockets and which is using HTMX. That’s amazing if you ask me. The HTMX implementation will also be cheaper in the long run as it requires no more infrastructure than you currently have.

If you want to check out the sample, you can get the solution on my GitHub repository and a few more samples of using HTMX with Blazor.

Conclusion

Server-rendered components are an excellent addition to the Blazor toolbox and open up the possibility of using your component library with something as cool as HTMX. I hope you try this sample and let me know what you think.

As always, thanks for reading my blog posts. Cheers.