One of the most persistent friction points when moving from a Single Page Application (SPA) architecture back to server-side rendering is "Action at a Distance."
In a React/Redux context, updating a shopping cart count in the navbar when a user clicks a button in the footer is trivial: dispatch an action, update the store, and let the components re-render. In a traditional request/response cycle, this usually implies a full page reload.
HTMX offers a mechanism to solve this without client-side state management or page reloads: Out-of-Band (OOB) Swaps.
The Architecture Mismatch
The root cause of this difficulty lies in the fundamental constraint of standard HTTP interactions: One Request, One Response, One Update.
When using HTMX in its default state, a user interaction (like a click) triggers an AJAX request. The server returns a snippet of HTML, and HTMX swaps that snippet into a specific target in the DOM.
The problem arises when a single action impacts multiple, disparate parts of the DOM tree. If the user clicks "Add to Cart" inside a product card (<main>), we naturally want to update the button to say "Added." However, the cart counter is physically located in the <nav>, typically outside the parent hierarchy of the product card.
Using standard hx-target, you can only update one contiguous DOM element. You cannot target both the button and the navbar simultaneously with a standard swap. This often leads developers to resort to client-side events (hx-trigger), which re-introduces the complexity of managing state synchronization on the client.
The Fix: Out-of-Band Swaps
The solution is to utilize the hx-swap-oob attribute. This feature allows the server to piggyback multiple HTML fragments in a single HTTP response. HTMX will take the primary content and swap it into the designated target, while simultaneously scanning the response for elements marked with hx-swap-oob. If found, it swaps those elements into the DOM wherever an element with a matching id exists.
Below is a complete implementation using Go and standard HTML/HTMX.
1. The Initial HTML Structure
We establish two distinct areas in our DOM: the global navigation (containing the cart count) and the product list. Note that the cart count has a specific ID: cart-indicator.
<!DOCTYPE html>
<html lang="en">
<head>
<title>HTMX Store</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<!-- 1. The Navbar (Target A) -->
<nav style="display: flex; justify-content: space-between; padding: 1rem; background: #333; color: #fff;">
<div class="brand">GoStore</div>
<!-- This ID is critical for the OOB swap -->
<div id="cart-indicator">
Cart: 0 items
</div>
</nav>
<!-- 2. The Main Content (Target B) -->
<main style="padding: 2rem;">
<div class="product-card">
<h3>Mechanical Keyboard</h3>
<p>$150.00</p>
<!--
hx-target="this": Replaces the button itself with the response.
hx-swap="outerHTML": Replaces the entire button tag.
-->
<button
hx-post="/cart/add"
hx-target="this"
hx-swap="outerHTML"
style="padding: 10px 20px; cursor: pointer;">
Add to Cart
</button>
</div>
</main>
</body>
</html>
2. The Server-Side Logic (Go)
The backend handles the POST. Instead of returning JSON or a single HTML fragment, it returns a concatenated stream of HTML containing two fragments.
- The updated button state (Main Target).
- The updated cart count (OOB Target).
package main
import (
"fmt"
"log"
"net/http"
"sync/atomic"
)
// Global state simulation
var cartCount int32 = 0
func main() {
http.HandleFunc("/", homeHandler)
http.HandleFunc("/cart/add", addToCartHandler)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
// In a real app, you would parse the index.html file here.
// For brevity, we serve the static file logic assumed in section 1.
http.ServeFile(w, r, "index.html")
}
func addToCartHandler(w http.ResponseWriter, r *http.Request) {
// 1. Mutate State (Simulate DB update)
newCount := atomic.AddInt32(&cartCount, 1)
// 2. Define the fragments
// Fragment A: The primary response (The Button)
// This replaces the button that triggered the request because hx-target="this"
primaryFragment := `
<button style="padding: 10px 20px; background-color: #d4edda; border: 1px solid #c3e6cb;">
Added!
</button>`
// Fragment B: The Out-of-Band Swap (The Navbar Count)
// 'hx-swap-oob="true"' tells HTMX to find #cart-indicator in the current DOM
// and replace it with this HTML.
oobFragment := fmt.Sprintf(`
<div id="cart-indicator" hx-swap-oob="true">
Cart: %d items
</div>`, newCount)
// 3. Write both fragments to the response body
w.Header().Set("Content-Type", "text/html")
// The order generally doesn't matter to HTMX, but readability suggests primary first.
fmt.Fprint(w, primaryFragment)
fmt.Fprint(w, oobFragment)
}
How It Works Under the Hood
When the user clicks "Add to Cart", the following sequence occurs:
- Request: HTMX issues a
POSTto/cart/add. - Processing: The Go server increments the atomic counter.
- Response: The server sends back a text stream containing:
<button ...>Added!</button> <div id="cart-indicator" hx-swap-oob="true">Cart: 1 items</div> - Parsing: HTMX receives the response. It identifies the element that triggered the request (the button) as the primary target.
- The Swap:
- HTMX takes the first fragment (the button) and swaps it into the DOM at the triggering element's location (standard behavior).
- HTMX detects the
hx-swap-oob="true"attribute on the second fragment. - It scans the existing DOM for an element with
id="cart-indicator". - It completely replaces the existing navigation DOM node with the new HTML fragment provided in the response.
Why This Matters
This approach decouples the UI representation from client-side logic. The server remains the single source of truth. The browser does not need to know that clicking a button increments a number; it only needs to know how to render the HTML the server provides.
This eliminates the need for:
- Redux/Zustand stores.
- Client-side event buses.
useEffecthooks monitoring state changes.- Manual DOM manipulation via
document.getElementById.
Conclusion
HTMX Out-of-Band swaps allow backend developers to orchestrate complex UI updates involving multiple DOM locations in a single HTTP cycle. By shifting the responsibility of state synchronization back to the server, we drastically reduce the JavaScript footprint and complexity of the application, while maintaining the responsive feel of an SPA.