Home Features Docs Blog Philosophy Examples FAQ
Documentation

Hot View Replacement

State-preserving Python code reload in development (React Fast Refresh parity)

Full documentation is on docs.djust.org

This page is a lightweight reference. The complete guide — with tutorials, theming, code examples, and more — lives on our dedicated docs site.

View on docs.djust.org

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

Auto-enabled in DEBUG since v0.9.0. Add djust to INSTALLED_APPS, keep DEBUG=True, install watchdog, and HVR works. No AppConfig.ready() boilerplate needed.

# settings.py (defaults shown — all True out of the box)
LIVEVIEW_CONFIG = {
    "hot_reload": True,                # file watcher on
    "hot_reload_auto_enable": True,    # call enable_hot_reload() from DjustConfig.ready()
    "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

Manual opt-in / advanced control

The auto-enable is on by default. If you orchestrate the file watcher externally (e.g. watchfiles wrapping uvicorn, or a custom dev loop), opt out with:

# settings.py
LIVEVIEW_CONFIG = {"hot_reload_auto_enable": False}

You can still call enable_hot_reload() yourself from any AppConfig. The function is idempotent — calling it manually is a safe no-op when the server is already running, so explicit calls coexist with the auto-enable.

# myapp/apps.py — only needed if hot_reload_auto_enable is False
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from djust import enable_hot_reload
        enable_hot_reload()

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:

Change Reason
__slots__ layout changed Python can't reassign __class__
An @event_handler method was removed Old bound calls would fail
A handler's positional-parameter names changed Bound args would mismatch
A class was deleted from the module Live instances would be orphaned
The file has a SyntaxError Old 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

Capability React Fast Refresh djust HVR
Preserves state on code change yes yes
Preserves scroll/input/DOM state yes yes
Incremental compile step required none
State-compat check hook rules __slots__ + handler signatures
Falls back to full reload on incompat yes yes
Dev-only (zero prod cost) yes yes

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.