---
title: "Common mistakes"
description: "Five mistakes that are easy to make with djust, each with the wrong code, the right code, and why."
level: beginner
order: 3.5
---

# Common mistakes

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.

```python
# views.py — wrong
@event_handler
def go_to_item(self, item_id: int = 0, **kwargs):
    self.live_redirect(f"/items/{item_id}/")
```

```html
<!-- 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.

```html
<!-- 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.

```python
# 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.

```python
# 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](/reference/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.

```html
<!-- 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](/guides/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.

```html
<!-- 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`.

```python
# 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.

```python
# 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](/guides/services/) covers the patterns in
full.

## 5. Event handlers without `**kwargs`

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

```python
# views.py — wrong
@event_handler
def delete_item(self, item_id: int = 0):
    Item.objects.filter(id=item_id).delete()
```

```html
<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.

```python
# 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": 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](/reference/config/) for every key.
- **Open the [debug panel](/advanced/debug-panel/)** to watch events and
  WebSocket traffic in the browser.
