---
title: "State & Computation Primitives"
slug: state-primitives
section: guides
order: 18
level: intermediate
description: "Memoized @computed, automatic dirty tracking (is_dirty / changed_fields), stable IDs (unique_id), and component context sharing (provide_context / consume_context) — small primitives that close the gap with React's useMemo / useId / Context."
---

# State & Computation Primitives

Four small primitives shipped together in v0.5.1 to close gaps with
the equivalent React hooks. Each one is opt-in: skip them and your
LiveView still works exactly as before.

| Primitive | React equivalent | Use case |
|---|---|---|
| `@computed("dep")` | `useMemo` | Cache expensive derived values; recompute only on dep change |
| `is_dirty` / `changed_fields` / `mark_clean()` | n/a | "Unsaved changes" warnings, conditional save buttons |
| `self.unique_id(suffix="")` | `useId` | Stable form-field / `aria-labelledby` IDs across renders |
| `self.provide_context()` / `consume_context()` | Context API | Share a value with descendant components without prop drilling |

---

## Memoized `@computed("dep1", "dep2")`

Plain `@computed` evaluates on every access — fine for cheap
properties. With dependency names, the value is **cached on the
instance** and only recomputed when any listed dep changes (identity
or shallow content).

```python
from djust import LiveView
from djust.decorators import computed, state


class CartView(LiveView):
    tax_rate = state(default=0.0825)

    def mount(self, request, **kwargs):
        # A fresh list per view instance. state() takes only `default=`
        # (there is no `default_factory`), and a mutable default would be
        # shared by every instance.
        self.items = []

    @computed("items", "tax_rate")
    def total(self):
        # Only recomputes when items or tax_rate changes.
        subtotal = sum(item["price"] * item["qty"] for item in self.items)
        return subtotal * (1 + self.tax_rate)
```

**When the cache invalidates.** A dep is "changed" when its identity
differs OR its structural fingerprint differs — the same
`deep_fingerprint` that drives re-rendering everywhere else in djust.
Plain containers (dict / list / tuple / set) are walked to their
leaves, so an in-place edit such as `self.items[0]["qty"] = 2` or
`self.columns["done"].append(card)` invalidates the cache **and**
re-renders the view on its own; you do not need to rebuild the list.

What the fingerprint cannot see is an attribute write on an *opaque*
object held in state — a model instance, a form, any non-container
object — which is compared by identity (`self.rows[0].name = "x"`
renders nothing). For those, assign a new value or call
`self.set_changed_keys("rows")` to force a re-render — see the State
Management API reference. A container so large it exceeds the
fingerprint budget (20 000 nodes) is reported once per view class with
the attribute name in the warning, and needs the same hatch.

**Skip the cache.** Use plain `@computed` (no args) when the
computation is cheap enough that property semantics are fine — every
access recomputes:

```python
@computed
def display_name(self):
    return f"{self.first_name} {self.last_name}"
```

---

## Automatic dirty tracking

After `mount()`, djust captures a baseline of every plain public attr
(one assigned directly on the instance, e.g. in `mount()`). From that
point on:

- `self.changed_fields` — set of attr names that differ from baseline.
- `self.is_dirty` — `bool(self.changed_fields)`.
- `self.mark_clean()` — reset the baseline (call after a successful save).

```python
from djust import LiveView
from djust.decorators import event_handler


class ProfileView(LiveView):
    # Plain attributes, not state(): dirty tracking doesn't see state() fields.
    def mount(self, request, **kwargs):
        self.first_name = request.user.first_name
        self.last_name = request.user.last_name

    @event_handler
    def save(self, **kwargs):
        user = self.request.user
        user.first_name = self.first_name
        user.last_name = self.last_name
        user.save()
        self.mark_clean()       # baseline now matches the saved state
```

Dirty tracking covers every public field: plain attributes, fields declared
with `state()`, and class-level components (a change to their `State` marks
the component's name as changed). Private (`_`-prefixed) fields and names in
`static_assigns` are never reported.

```django
{# template — show a Save button only when there's work to save #}
{% if is_dirty %}
  <button dj-click="save">Save changes</button>
  <p class="hint">{{ changed_fields|length }} field(s) changed</p>
{% else %}
  <p class="hint">All changes saved.</p>
{% endif %}
```

The tracker respects `static_assigns` (constants the framework
shouldn't watch) and ignores private (`_`-prefixed) attrs. Both the
WebSocket consumer and the HTTP API dispatch view capture the
baseline post-mount, so dirty tracking works identically over both
transports.

### Common patterns

- **`beforeunload` warning**: render `<body data-dirty="{{ is_dirty }}">`,
  hook `beforeunload` in your base template to read the attr and
  warn the user.
- **Conditional save**: `{% if is_dirty %}` around the submit button.
- **Skip work in `handle_event`**: `if not self.is_dirty: return` at
  the top of an autosave handler.

---

## Stable `self.unique_id(suffix="")`

Returns a deterministic ID stable across re-renders of the same logical
element position. Format: `djust-<viewslug>-<n>[-<suffix>]`.

```python
class FormView(LiveView):
    def mount(self, request, **kwargs):
        self.email_id = self.unique_id("email")
        self.email_help_id = self.unique_id("email-help")
```

```django
<label for="{{ email_id }}">Email</label>
<input id="{{ email_id }}" aria-describedby="{{ email_help_id }}">
<p id="{{ email_help_id }}">We never share your email.</p>
```

The counter resets per render boundary via the framework's
`reset_unique_ids()` hook, so the same call in `mount()` always
returns the same ID across re-renders. Two views of *different
classes* on the same page get distinct IDs because the slug (the
lowercased class name) differs. Two instances of the *same* class share
the slug, so their IDs collide.

When to reach for `unique_id()`:

- `aria-labelledby` / `aria-describedby` — accessibility associations
  that need stable IDs across renders.
- `<label for="…">` paired with `<input id="…">` for non-Form views
  (FormMixin already generates stable IDs for Django form fields).
- Any element another script or stylesheet selects by ID.

Don't use it for routing keys (those should derive from data) or
session-scoped IDs (use the request session instead).

---

## Component context sharing

`provide_context(key, value)` exposes a value to all descendants of
the current view or component; `consume_context(key, default=None)`
walks the parent chain and returns the value (or `default` if none
provided it).

```python
class ThemedAppView(LiveView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        # Provide AFTER super(): each render clears the providers first.
        self.provide_context("theme", self.request.user.preferences.theme)
        self.provide_context("locale", self.request.LANGUAGE_CODE)
        return ctx


# In a deeply-nested LiveComponent:
class AccentBadge(LiveComponent):
    def get_context_data(self):
        theme = self.consume_context("theme", default="light")
        return {"theme": theme}
```

The lookup walks `_djust_context_parent` upward until a provider is
found — no prop drilling required. Scope is per render tree:
`get_context_data()` calls `clear_context_providers()` at the start of
every render, so providers set in `mount()` or in an event handler are
wiped before any component reads them. Provide context in
`get_context_data()`, after calling `super()`, as above.

When to reach for context vs explicit props:

- **Context** — values used by many descendants at varying depths
  (theme, locale, current user, feature flags).
- **Explicit props / assigns** — values used only by one or two
  immediate children (the `Assign(...)` DSL is more discoverable
  and runtime-validates).

---

## See also

- [Components guide](components.md) — the `Assign` / `Slot` DSL for
  declarative props on `LiveComponent` and function components.
- [Hooks guide](hooks.md) — when you really do need a JS-side hook
  instead of a server-side primitive.
