Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dev / HMR — reference

std::dev is the app-facing surface of hot module replacement — the live-update loop vilan run --watch runs for a full-stack app (see The dev loop for the whole picture). Browser-only, and every hook here is a no-op outside a hot reload: importing it costs nothing in a production build, so you can leave the calls in place.

fun hmr_active(): bool                       // is a hot-reload session live?
fun on_teardown(cleanup: || void)            // run before the next swap
fun stash<type T>(key: str, value: T)        // carry plain data across a swap
fun take<type T>(key: str): Option<T>        // recover it (None outside HMR / on a miss)

hmr_active

True only while a run --watch browser round has installed its dev runtime; false in a normal build. Everything else in the module already guards on it, so you rarely call it directly — reach for it when you want a whole block to exist only during development.

import std::print;
import std::dev;

fun main() {
	if dev::hmr_active() {
		print("editing live — the dev channel is connected");
	}
}

on_teardown

Register a cleanup to run before the next hot swap re-evaluates the bundle. The swap’s teardown disposes the UI root and closes the live rpc socket for you; on_teardown is the sanctioned patch for anything the swap can’t see on its own — a raw set_interval, a bare spawned task, a handle to something outside the reactive system. Without it, that stray keeps running after the swap: harmless (it writes into disposed cells) but wasteful.

import std::dev;
import std::shared::Shared;

let connection: Shared<bool> = Shared::new(true);

fun main() {
	// Something the reactive runtime doesn't own — close it before a swap so
	// the re-evaluated bundle starts from a clean slate.
	dev::on_teardown(|| connection.write() = false);
}

A plain browser refresh is always the complete reset — seed state lives only in the page’s heap — so on_teardown is about tidiness within a session, not correctness across one.

stash / take

The manual carryover channel — Vite’s hot.data, made type-safe. stash a value under a key before a swap; take it back in the re-evaluated bundle. Module-level bindings carry across a swap automatically (see the dev loop); stash/take is for the rest — a value minted inside a function that you want to survive one edit.

import std::print;
import std::dev;
import std::option::Option::{ self, Some, None };

fun main() {
	// On the first boot `take` is None; after a hot swap it returns what the
	// previous bundle stashed. `take` is non-destructive — the value stays put
	// for the swap after this one too (Vite's persistent `hot.data`).
	let seen: Option<i32> = dev::take("visits");
	let visits = match seen {
		Some(let n) => n + 1,
		None => 1,
	};
	print(i"loaded {visits} time(s) this session");
	dev::stash("visits", visits);
}

The transfer bound. T must be transferable-as-value: plain data the new bundle can adopt by reference without inheriting old code — scalars, str, List, Option, Result, tuples, and structs/enums built from them. A closure, a View, a resource, or a reactive cell (Signal/Shared) carries code or identity the new bundle can’t adopt, so stashing one is a compile error at the call site — the type system enforces what Vite leaves to convention:

let live: Signal<i32> = Signal::new(0);
dev::stash("live", live);
// error: `Signal<i32>` cannot cross a hot swap — a closure, view, resource,
// or reactive cell (`Signal`/`Shared`) carries code or identity the new
// bundle cannot adopt; stash only plain data

To carry the value inside a Signal, stash its payload — stash("n", signal.get()) — and re-seed a fresh cell from take on the other side. That is exactly what the compiler does for you for a module-level Signal binding; stash/take just gives you the same move by hand for the cases it can’t reach.

take returns None on a first boot, a plain browser refresh, or a key nothing has stashed. One honest caveat: unlike the automatic module-binding carryover — which fingerprints each binding’s type and fresh-inits on a mismatch — the manual stash is a plain keyed slot. If an edit changes the type you take at a key, the old value comes back in its old shape; the transfer bound guarantees it is still plain data, but reading it as the new type is on you (Vite’s hot.data has the same contract, unchecked). When in doubt, pick a new key — or refresh, which clears the stash entirely.