djust docs
Browse documentation

Common mistakes

Five mistakes that are easy to make with djust, each with the wrong code, the right code, and why.

On this page

Five mistakes that come up again and again, each with what goes wrong and the fix. Several are caught for you: run python manage.py check — djust's system checks warn about event handlers missing **kwargs (djust.V007) and mark_safe() called on an f-string (djust.S001), among others.

1. Navigating from an event handler

Problem: a button whose handler does nothing but send the browser to another page.

# views.py — wrong
@event_handler
def go_to_item(self, item_id: int = 0, **kwargs):
    self.live_redirect(f"/items/{item_id}/")
<!-- template — wrong -->
<button dj-click="go_to_item" data-item-id="{{ item.id }}">View item</button>

Why it matters: every click makes a round trip to the server just to learn the URL, and a button cannot be opened in a new tab or have its address copied.

Fix: a link. dj-navigate moves to another view over the existing connection; dj-patch changes only the query string, without remounting. Keep the href as well: djust leaves Ctrl/Cmd-clicks and middle clicks to the browser, and the browser can only open a new tab for a link that has one.

<!-- template — right -->
<a href="/items/{{ item.id }}/" dj-navigate="/items/{{ item.id }}/">View item</a>

<!-- only the query string changes; a bare dj-patch uses the href -->
<a href="?filter={{ filter }}&sort=name" dj-patch>Sort by name</a>

(dj-navigate needs its own value: written bare, it does nothing and the link loads as a normal page.)

Navigate from a handler only when the destination depends on server work — after saving a record, checking a permission, or choosing where to go.

2. Calling live_navigate()

Problem: self.live_navigate(...) in a handler.

# views.py — wrong
@event_handler
def save_item(self, **kwargs):
    item = Item.objects.create(...)
    self.live_navigate(f"/items/{item.id}/")  # AttributeError: no such method

Why it happens: the template directive is dj-navigate, so the method name is an easy guess — but there is no live_navigate() on a LiveView.

Fix: live_redirect(path) goes to another view; live_patch(params=...) updates the query string and calls handle_params() without remounting.

# views.py — right
@event_handler
def save_item(self, **kwargs):
    item = Item.objects.create(...)
    self.live_redirect(f"/items/{item.id}/")

@event_handler
def sort_by(self, key: str = "name", **kwargs):
    self.live_patch(params={"sort": key})

Both are listed with every parameter in Navigation methods.

3. A root element that never gets its WebSocket

Problem: the page renders and even responds to clicks, but nothing the server sends on its own ever arrives, and no WebSocket is opened.

<!-- template — wrong: extra attributes on the root, no dj-view -->
<div dj-root class="counter">
    <h1>Count: {{ count }}</h1>
    <button dj-click="increment">+1</button>
</div>

Why it happens: the browser mounts a view over the WebSocket only for a root element that names its view in dj-view. djust fills that in for you only when the tag is exactly <div dj-root> — it matches that literal string. Add a class, or use another element, and no dj-view is written. The page then falls back to one HTTP POST per event (see HTTP-only mode), so clicks still work and nothing looks broken, but updates the server starts itself — ticks, broadcasts, pushed events — have no socket to arrive on.

Fix: write the root exactly as <div dj-root>, and style an element inside it — or keep your attributes and name the view yourself.

<!-- right: djust adds dj-view -->
<div dj-root>
    <div class="counter">
        <h1>Count: {{ count }}</h1>
        <button dj-click="increment">+1</button>
    </div>
</div>

<!-- also right: the view named explicitly -->
<div dj-root dj-view="myapp.views.CounterView" class="counter">
    ...
</div>

To check a page, look for WebSocket /ws/live/ in the server log when it loads, or for a dj-view attribute on the root in the rendered HTML.

4. Keeping service clients in view state

Problem: storing a client, session, connection or file handle on self.

# views.py — wrong
def mount(self, request, **kwargs):
    self.s3_client = boto3.client("s3")

@event_handler
def upload_file(self, **kwargs):
    self.s3_client.upload_file(...)

Why it matters: a view's state has to be serializable. djust logs a warning for a value that is not — LiveView state contains non-serializable value: ... — and converts it to a string wherever the state is serialized. As the warning itself says, that may cause an AttributeError once the state is restored: the attribute is now a str, not a client. It can work in development and fail later. With LIVEVIEW_CONFIG = {"strict_serialization": True} the same value raises TypeError straight away, which is the better default while developing.

Fix: create the client when you need it, in a private helper.

# views.py — right
def _s3(self):
    return boto3.client("s3")

@event_handler
def upload_file(self, **kwargs):
    self._s3().upload_file(...)

Working with external services covers the patterns in full.

5. Event handlers without **kwargs

Problem: a handler that lists only the parameters it uses.

# views.py — wrong
@event_handler
def delete_item(self, item_id: int = 0):
    Item.objects.filter(id=item_id).delete()
<button dj-click="delete_item" data-item-id="{{ item.id }}" data-confirm="true">
    Delete
</button>

Why it matters: every data-* attribute on the element arrives as a keyword argument (data-item-id as item_id, data-confirm as confirm). A handler without **kwargs rejects the event — Handler 'delete_item' received unexpected parameters: ['confirm'] — and the button does nothing.

Fix: end every handler's signature with **kwargs. The djust.V007 system check warns about any that do not.

# views.py — right
@event_handler
def delete_item(self, item_id: int = 0, **kwargs):
    Item.objects.filter(id=item_id).delete()

Debugging tips

  • Run the system checkspython manage.py check — after changing views or templates.
  • Log VDOM patches while developing: "debug_vdom": True in LIVEVIEW_CONFIG logs the patches each render produces on the server (at DEBUG level) and turns on the Rust renderer's VDOM tracing. See the Configuration reference for every key.
  • Open the debug panel to watch events and WebSocket traffic in the browser.