From htmx to Django LiveView
If you're using htmx and evaluating Django LiveView, this article is for you. I want to quickly show you the equivalences of what you usually do with htmx and how to achieve it with Django LiveView. It's not an exhaustive comparison, but a practical guide to migrate or decide which one to use.
Fundamental differences
Before looking at code, we need to be clear: architecturally they are very different. They don't even share the same communication protocol.
| Feature | htmx | Django LiveView |
|---|---|---|
| Protocol | HTTP/AJAX | WebSockets |
| Communication | Individual requests (GET, POST, etc) | Persistent connection |
| State | Stateless | Stateful |
| Real-time updates | No (polling required) | Yes (server push) |
| Infrastructure requirements | Minimal (HTTP server) | Moderate (Channels + Redis recommended) |
| Latency | Higher (each HTTP request) | Lower (persistent connection) |
| Broadcast support | No | Yes |
| Performance* | Good (16.48ms) | Excellent (9.35ms) |
| Requires | Views or REST API | Django Channels |
- Based on simple benchmarks with multiple users.
In practical terms, htmx is simpler to configure and use for basic interactions. Django LiveView is more powerful for real-time interactive applications, when you need to maintain server state, broadcast to multiple clients, or create SPA-type applications.
Installation and configuration
htmx
<!-- In your base template -->
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
Done. No server configuration.
Optionally, you can install django-htmx to facilitate working with htmx in Django. It provides middleware that detects htmx requests and simplifies header handling:
pip install django-htmx
# settings.py
MIDDLEWARE = [
# ... other middleware
'django_htmx.middleware.HtmxMiddleware',
]
With this, you can use request.htmx in your views to detect htmx requests and access specific headers.
Django LiveView
Requires Django Channels and an ASGI server like Daphne. Redis is not mandatory, but is highly recommended as otherwise you will lose some communication functionalities.
Basic installation (without Redis, testing only):
pip install django-liveview channels daphne
Recommended installation (with Redis, for development and production):
pip install django-liveview channels channels-redis daphne redis
# settings.py
INSTALLED_APPS = [
'daphne', # Must be first
'django.contrib.staticfiles',
# ... other apps
'liveview',
]
ASGI_APPLICATION = 'myproject.asgi.application'
# Option 1: Without Redis (local testing only)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
# Option 2: With Redis (recommended)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
},
},
}
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from liveview import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(routing.websocket_urlpatterns)
),
})
# urls.py
from django.urls import path, include
urlpatterns = [
path('', include('liveview.urls')),
# ... your urls
]
You need more packages and configurations. But as you'll see, the power you get in return is worth it.
Case 1: Update content with a click
The most basic example. A button that updates a div.
htmx
<!-- Template -->
<div id="content">Initial content</div>
<button hx-get="/update-content" hx-target="#content">
Update
</button>
# views.py
def update_content(request):
return HttpResponse("<p>Updated content</p>")
# urls.py
path('update-content/', update_content),
Django LiveView
<!-- Template -->
{% load liveview %}
<!DOCTYPE html>
<html data-liveview-room-uuid="{% liveview_room_uuid %}">
<head>
{% liveview_headers %}
</head>
<body>
<div id="content">{{ content }}</div>
<button data-liveview-function="update_content"
data-action="click->page#run">
Update
</button>
</body>
</html>
# handlers.py
from liveview import liveview_handler
@liveview_handler("update_content")
def update_content(consumer, content):
return {
"target": "#content",
"html": "<p>Updated content</p>",
}
Notice how htmx needs a specific route, while LiveView handles everything via WebSocket with a decorator.
Case 2: Form with validation
A form that validates on the server without reloading the page.
htmx
<!-- Template -->
<form hx-post="/validate-form" hx-target="#errors">
<input type="email" name="email">
<div id="errors"></div>
<button type="submit">Submit</button>
</form>
# views.py
def validate_form(request):
email = request.POST.get('email')
if not email or '@' not in email:
return HttpResponse('<p style="color:red;">Invalid email</p>')
return HttpResponse('<p style="color:green;">Valid email</p>')
# urls.py
path('validate-form/', validate_form),
Django LiveView
<!-- Template -->
{% load liveview %}
<form data-liveview-function="validate_form"
data-action="submit->page#run">
<input type="email" name="email" id="email-input">
<div id="errors">{{ error_message }}</div>
<button type="submit">Submit</button>
</form>
# handlers.py
from liveview import liveview_handler
@liveview_handler("validate_form")
def validate_form(consumer, content):
email = content.get('email', '')
if not email or '@' not in email:
error_html = '<p style="color:red;">Invalid email</p>'
else:
error_html = '<p style="color:green;">Valid email</p>'
return {
"target": "#errors",
"html": error_html,
}
With Django LiveView you can access the form state directly from the content object without having to use request.POST.
Case 3: Real-time search
Search that updates as you type.
htmx
<!-- Template -->
<input type="text"
name="query"
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#results">
<div id="results"></div>
# views.py
def search(request):
query = request.GET.get('query', '')
results = Article.objects.filter(title__icontains=query)[:5]
return render(request, 'search_results.html', {'results': results})
# urls.py
path('search/', search),
The delay:500ms attribute prevents making requests on every key press.
Django LiveView
<!-- Template -->
{% load liveview %}
<input type="text"
name="query"
id="search-input"
data-liveview-function="search"
data-action="input->page#run"
data-liveview-debounce="500">
<div id="results">{% include 'search_results.html' %}</div>
# handlers.py
from django.template.loader import render_to_string
from liveview import liveview_handler
from .models import Article
@liveview_handler("search")
def search(consumer, content):
query = content.get('query', '')
results = Article.objects.filter(title__icontains=query)[:5]
html = render_to_string('search_results.html', {'results': results})
return {
"target": "#results",
"html": html,
}
The data-liveview-debounce="500" attribute serves the same function as delay:500ms in htmx.
Case 4: Automatic update (polling)
Content that updates periodically.
htmx
<!-- Template -->
<div hx-get="/stats"
hx-trigger="every 2s"
id="stats">
{{ stats }}
</div>
# views.py
def stats(request):
active_users = get_active_users()
return HttpResponse(f'<p>Active users: {active_users}</p>')
# urls.py
path('stats/', stats),
Django LiveView
Django LiveView doesn't have automatic client-side polling. The philosophy is different: the server broadcasts when there are changes. But you can simulate it with a thread on the server:
# handlers.py
import threading
from time import sleep
from liveview import liveview_handler
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def poll_stats():
"""Thread that updates stats every 2 seconds"""
channel_layer = get_channel_layer()
while True:
sleep(2)
active_users = get_active_users()
html = f'<p>Active users: {active_users}</p>'
async_to_sync(channel_layer.group_send)(
'broadcast',
{
'type': 'broadcast_message',
'message': {
'target': '#stats',
'html': html,
}
}
)
# Start thread when app starts
threading.Thread(target=poll_stats, daemon=True).start()
<!-- Template -->
<div id="stats">{{ stats }}</div>
This approach is more powerful because all connected clients receive the update simultaneously, without each one doing individual polling.
Case 5: SPA navigation
Navigate without reloading the entire page.
htmx
<!-- Base template -->
<nav hx-boost="true">
<a href="/about">About us</a>
<a href="/contact">Contact</a>
</nav>
<div id="content">
<!-- Content -->
</div>
# views.py
def about(request):
return render(request, 'about.html')
def contact(request):
return render(request, 'contact.html')
# urls.py
path('about/', about),
path('contact/', contact),
hx-boost="true" converts normal links into AJAX requests. htmx intercepts the click, makes a GET request and replaces the <body> with the response content.
Django LiveView
<!-- Template -->
{% load liveview %}
<nav>
<a href="#"
data-liveview-function="load_about"
data-action="click->page#run">
About us
</a>
<a href="#"
data-liveview-function="load_contact"
data-action="click->page#run">
Contact
</a>
</nav>
<div id="content">
<!-- Content -->
</div>
# handlers.py
from django.template.loader import render_to_string
from liveview import liveview_handler
@liveview_handler("load_about")
def load_about(consumer, content):
html = render_to_string('about.html')
return {
"target": "#content",
"html": html,
}
@liveview_handler("load_contact")
def load_contact(consumer, content):
html = render_to_string('contact.html')
return {
"target": "#content",
"html": html,
}
Case 6: Shared state between users
Multiple users viewing real-time data.
htmx
No native support. You need to implement Server-Sent Events (SSE) or WebSockets manually, which breaks the htmx model.
Django LiveView
# handlers.py
from liveview import liveview_handler
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
@liveview_handler("add_message")
def add_message(consumer, content):
message_text = content.get('message', '')
channel_layer = get_channel_layer()
# Broadcast to all connected users
async_to_sync(channel_layer.group_send)(
'chat_room',
{
'type': 'broadcast_message',
'message': {
'target': '#messages',
'html': f'<p>{message_text}</p>',
}
}
)
<!-- Template -->
<div id="messages">
<!-- Messages appear here -->
</div>
<form data-liveview-function="add_message"
data-action="submit->page#run">
<input type="text" name="message">
<button type="submit">Send</button>
</form>
All connected users receive the message instantly.
Case 7: File handling
htmx
<form hx-post="/upload"
hx-encoding="multipart/form-data"
hx-target="#result">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
<div id="result"></div>
# views.py
def upload(request):
if request.method == 'POST':
uploaded_file = request.FILES['file']
# Process file
return HttpResponse(f'<p>File {uploaded_file.name} uploaded</p>')
# urls.py
path('upload/', upload),
Django LiveView
{% load liveview %}
<form data-liveview-function="upload_file"
data-action="submit->page#run"
enctype="multipart/form-data">
<input type="file" name="file" id="file-input">
<button type="submit">Upload</button>
</form>
<div id="result"></div>
# handlers.py
from liveview import liveview_handler
@liveview_handler("upload_file")
def upload_file(consumer, content):
uploaded_file = content.get('file')
if uploaded_file:
# Process file
return {
"target": "#result",
"html": f'<p>File {uploaded_file.name} uploaded</p>',
}
Functionally similar, but over WebSocket.
Practical migration
If you decide to migrate from htmx to LiveView:
- Install basic dependencies:
django-liveview,channels,daphne(and optionallychannels-redis+ Redis for production) - Configure ASGI: Modify settings.py and asgi.py according to the installation section
- Configure channel layers: Use
InMemoryChannelLayerfor testing orRedisChannelLayerfor production - Convert views to handlers: Each view that returns partial HTML becomes a handler with
@liveview_handler - Update templates: Replace
hx-*attributes withdata-liveview-*anddata-action. Also add the necessary tags for LiveView
You can keep SSR for some pages, htmx in some components and use LiveView in others. One technology doesn't exclude the other.
There is no wrong choice. Both technologies are complementary and seek maximum simplicity within their paradigms. Choose the one that best suits your needs and scale from there. Happy Hacking!
- Fundamental differences
- Installation and configuration
- htmx
- Django LiveView
- Case 1: Update content with a click
- htmx
- Django LiveView
- Case 2: Form with validation
- htmx
- Django LiveView
- Case 3: Real-time search
- htmx
- Django LiveView
- Case 4: Automatic update (polling)
- htmx
- Django LiveView
- Case 5: SPA navigation
- htmx
- Django LiveView
- Case 6: Shared state between users
- htmx
- Django LiveView
- Case 7: File handling
- htmx
- Django LiveView
- Practical migration
This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.
Comments
There are no comments yet.