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 checks —
python manage.py check— after changing views or templates. - Log VDOM patches while developing:
"debug_vdom": TrueinLIVEVIEW_CONFIGlogs the patches each render produces on the server (atDEBUGlevel) 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.