Skip to content
djust
Appearance
Mode

Hot View Replacement

New in v0.6.1. Edit a LiveView's Python file, hit save, and your browser updates without losing form input, counter values, scroll position, or open tabs. This is React Fast Refresh parity for djust.

Gated on DEBUG=True and enabled by default. Zero cost in production.

Quick start

Hot View Replacement (HVR) rides on top of the existing enable_hot_reload() machinery. If you already enabled hot reload, HVR is already working — no new config needed.

# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from django.conf import settings
        if settings.DEBUG:
            from djust import enable_hot_reload
            enable_hot_reload()
# settings.py (defaults shown)
LIVEVIEW_CONFIG = {
    "hot_reload": True,    # file watcher on
    "hvr_enabled": True,   # v0.6.1 — state-preserving reload
}

Make sure watchdog is installed — djust_check will print C401 if it's missing:

pip install watchdog

What gets preserved

When you edit a LiveView's Python file:

  1. The file-watcher detects the change.
  2. djust calls importlib.reload() on the module.
  3. Every connected consumer's view instance has its __class__ swapped to the new class, in place, so instance.__dict__ is untouched.
  4. A VDOM diff runs against the new class's render output and the patches stream to the browser.
  5. The client dispatches a djust:hvr-applied CustomEvent and (in debug mode) paints a small green toast in the bottom-right.

The user keeps:

  • form field values
  • scroll position
  • counter values / any public attribute on the view
  • active tab, open modal, etc.
  • child view state (sticky LiveViews are recursed through)

When it falls back to a full reload

HVR is conservative. If any of the following changed between saves, djust emits a plain {"type": "reload"} frame and the page refreshes:

ChangeReason
__slots__ layout changedPython can't reassign __class__
An @event_handler method was removedOld bound calls would fail
A handler's positional-parameter names changedBound args would mismatch
A class was deleted from the moduleLive instances would be orphaned
The file has a SyntaxErrorOld code stays live until you fix it

Everything else swaps cleanly:

  • Adding new @event_handler methods
  • Changing handler bodies (e.g. self.count += 1self.count += 2)
  • Changing template, template_name, page_meta, tick_interval
  • Adding public attributes
  • Editing mount() (not re-run on swap, but affects next mount)

React Fast Refresh parity

CapabilityReact Fast Refreshdjust HVR
Preserves state on code changeyesyes
Preserves scroll/input/DOM stateyesyes
Incremental compile steprequirednone
State-compat checkhook rules__slots__ + handler signatures
Falls back to full reload on incompatyesyes
Dev-only (zero prod cost)yesyes

Observing HVR from application code

Every successful swap dispatches a CustomEvent on document:

document.addEventListener("djust:hvr-applied", (ev) => {
    console.log("HVR applied:", ev.detail.view, "v" + ev.detail.version);
});

The detail object mirrors the server frame:

{
    "type": "hvr-applied",
    "view": "app.views.Dashboard",
    "version": 3,
    "file": "/abs/path/to/app/views/dashboard.py"
}

Use this to clear caches, re-wire third-party widgets, or just log reloads in your own dev overlay.

Limitations

  • Single-process only. If you run multiple Gunicorn/Uvicorn workers in dev, only the process that reloaded the module sees the new class. Use a single worker in dev (the djust dev server does this by default).
  • No Rust code reload. Edits to the Rust crates require a wheel rebuild; make dev-build + restart.
  • No models.py reload for migration. HVR reloads the module in memory but doesn't run migrate. Restart if you changed models.
  • No mixin slot growth. Adding a new __slots__ entry to a mixin always triggers a full reload.
  • Mixin edits trigger a full reload. HVR only reloads the module whose file was saved. Editing a mixin's source file does not propagate to views that inherit from it until you also save those views' files — and if the mixin's MRO contribution changes, HVR will reject the swap (mro_changed) and fall back to a full page reload.

What HVR doesn't do (caveats)

HVR runs importlib.reload() on the edited module, which re-executes the module body. Any side effects at module top level fire again:

  • signals.connect(...) calls — will register a second receiver and fire handlers twice on the next save.
  • URL.register(...) / custom registry appends.
  • threading.Thread(...).start() — spawns a fresh thread every save.
  • Top-level print(...) / logging calls — fire per save.

Keep the module top-level pure (class definitions and imports only). Move side effects into AppConfig.ready(), a guarded if not hasattr(module, "_djust_initialized") block, or an explicit @lru_cache()-wrapped init helper. This is standard Django hygiene (AppConfig.ready() exists for exactly this reason), and following it makes HVR behave the same as a cold start.

Configuration

LIVEVIEW_CONFIG = {
    # Master switch (default True). Disable to restore pre-v0.6.1
    # behavior (full page reload on every .py change).
    "hvr_enabled": True,

    # File-watcher dirs (unchanged from hot_reload).
    "hot_reload_watch_dirs": None,   # None = settings.BASE_DIR
    "hot_reload_exclude_dirs": None,
}

Troubleshooting

"HVR applied" toast never appears. Check that globalThis.djustDebug = true is set in your dev console — the toast is debug-mode-only. Check djust_check output for C401 (watchdog missing).

Full reload on every save. Look for HVR incompat: <reason> in the Django log. Most common: you renamed a handler parameter or added a __slots__ entry. Save once to pay the full reload, then subsequent saves should swap in place.

Stale class after reload. If you see ImportError in the log, the module import itself failed — the old class stays live. Fix the SyntaxError / import and resave.