djust docs
Browse documentation

Your First LiveView

On this page

Build a live counter — no page refreshes, no JavaScript to write.

What You'll Build

A counter with increment/decrement buttons that updates instantly via WebSocket. The entire feature is Python.

Before you start

Complete Installation first. Run commands below from the directory containing manage.py, with the project environment activated (or prefix them with .venv/bin/ or uv run).

The manual installation path already creates and registers myapp. If you used djust new myproject, the generated starter app is named myproject; create a separate app for this tutorial:

python manage.py startapp myapp

Add "myapp" to INSTALLED_APPS in your project's settings, and add "myapp.views" to LIVEVIEW_ALLOWED_MODULES if your settings define it (djust init does not; without the setting, installed apps are allowed). Keep the existing entries. The URL step below connects this app to the site.

1. Create the View

Create myapp/views.py:

from djust import LiveView
from djust.decorators import event_handler


class CounterView(LiveView):
    template_name = "myapp/counter.html"

    def mount(self, request, **kwargs):
        """Initialize state here. Runs on the first HTTP render and again when the WebSocket connects."""
        self.count = 0

    def get_context_data(self, **kwargs):
        """Return template context. Called before every render."""
        return {"count": self.count}

    @event_handler()
    def increment(self, **kwargs):
        self.count += 1

    @event_handler()
    def decrement(self, **kwargs):
        self.count -= 1

Key rules:

  • mount() runs on the initial HTTP render and again when the WebSocket connects (unless state is restored) — set initial state here, not in __init__, and keep it idempotent (no one-time side effects)
  • Every event handler needs @event_handler() — djust blocks undecorated methods for security
  • Declare the parameters each binding sends; manage.py check reports a handler that would reject one (djust.T020)
  • State lives on self — any change to self.count triggers a re-render automatically

2. Create the Template

Create myapp/templates/myapp/counter.html:

<!DOCTYPE html>
<html>
<head>
    <title>Counter</title>
    {% load live_tags %}
    {% djust_client_config %}
</head>
<body>
    <div dj-root>
        <h1>Count: {{ count }}</h1>

        <button dj-click="decrement">-</button>
        <button dj-click="increment">+</button>
    </div>
</body>
</html>

Template requirements:

  • {% load live_tags %} and {% djust_client_config %} emit client config meta tags; djust auto-injects the client JS (~70 KB gz) into every LiveView response
  • dj-root marks the reactive region — only this subtree is patched on updates. It is the only root attribute you write; djust stamps dj-view onto it server-side with the dotted path of the view rendering the page, which is what connects the page to the WebSocket session
  • (Write dj-view="myapp.views.MyView" yourself only to name a specific view — an embedded or sticky view, or a template shared by several views. It is a literal path; there is no dj_view_id variable)
  • dj-click="increment" binds a click event to the increment handler

3. Add a URL

In myapp/urls.py:

from django.urls import path
from myapp.views import CounterView

urlpatterns = [
    path("counter/", CounterView.as_view(), name="counter"),
]

In the project's myproject/urls.py, import include and add the app's routes to the existing urlpatterns if they are not already included:

from django.urls import include, path

urlpatterns += [path("", include("myapp.urls"))]

A scaffolded project can keep its starter homepage. The new counter is at /counter/.

4. Run It

python -m uvicorn myproject.asgi:application --reload

Visit http://localhost:8000/counter/ and click the buttons — the count updates instantly without a page reload.

How It Works

  1. The first request is a normal HTTP response (good for SEO and initial load)
  2. The page JS opens a WebSocket connection to /ws/live/
  3. When you click a button, the client sends {"type": "event", "event": "increment", "params": {}} over the WebSocket
  4. djust calls your increment() method, re-renders the template in Rust, diffs the VDOM, and sends only the changed HTML fragments back
  5. The client patches the DOM — no full page reload

Responding to Input

For text inputs, use dj-input (fires on the input event, debounced 300 ms by default for text-like fields; override with dj-debounce/dj-throttle) or dj-change (fires on blur):

@event_handler()
def search(self, value: str = "", **kwargs):
    """The 'value' parameter receives the current input value."""
    self.query = value
<input type="text" dj-input="search" value="{{ query }}" placeholder="Search..." />
<p>You searched for: {{ query }}</p>

Passing Data from the DOM

Use data-* attributes to pass data to event handlers:

@event_handler()
def delete_item(self, item_id: int = 0, **kwargs):
    """data-item-id="5" becomes item_id=5 (auto-converted to int)."""
    self.items = [i for i in self.items if i["id"] != item_id]
{% for item in items %}
<li>
    {{ item.name }}
    <button dj-click="delete_item" data-item-id="{{ item.id }}">Delete</button>
</li>
{% endfor %}

Next Steps

Try a real-world walkthrough

Once the counter is working, the four end-to-end tutorials apply the same primitives to common shipping patterns: