How the Native SDK Dev Workflow Enables Hot Reload While Preserving State
The native dev command watches .native markup sources via the watch_path field in canvas.ui_markup.MarkupOptions and re-evaluates the UI tree on every change while preserving widget provenance and leaving the underlying application model untouched.
The vercel-labs/native repository delivers a fast, iterative development loop for building cross-platform apps. The Native SDK dev workflow enables hot reload while preserving state by strictly separating declarative markup from imperative application logic, so developers can edit .native files and see instant visual updates without losing counters, selections, or network sessions.
How native dev Bootstraps the Watch Loop
When you run native dev inside a project directory, the CLI compiles the Zig or TypeScript core in debug mode and initializes a file watcher. In both src/tooling/templates.zig and src/app_runner/ts_core_main.zig, the generated entry point configures canvas.ui_markup.MarkupOptions with a watch_path pointing to the markup source:
.markup = .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io },
This single option tells the runtime which file to monitor for changes while the app is running.
Live Reload Inside the Runtime
The core reload logic lives in src/runtime/ui_app.zig. When the file specified by watch_path changes on disk, the runtime performs a targeted update rather than a full application restart.
Detecting File Changes via watch_path
The runtime receives the optional watch_path at startup. On every detected change, it invokes readMarkupFile to re-read the markup from disk, resolves any imports, and prepares a fresh UI tree for reconciliation.
Rebuilding the UI Tree
After the markup is parsed, the runtime reconstructs the view hierarchy. Because the application model lives entirely in Zig or TypeScript code—outside the markup layer—fields such as virtual variables, session stores, and network handles remain untouched while the visual surface is rebuilt.
Preserving Widget IDs Through Provenance
State survival depends on the provenance system. Inside ui_app.zig, the self.provenance.watching logic matches logical widgets across reloads by keeping their identities stable. Instead of destroying and recreating every widget, the runtime reconciles the old and new trees based on provenance IDs. This preserves internal widget state, focus, and scroll position.
The mechanism is verified by the UI-App test suite in src/runtime/ui_app_tests.zig, which writes temporary markup files, mutates them on disk, and asserts that widget IDs remain stable while the UI reflects the latest markup changes.
Automatic Model-Binding Refresh
Because the markup is re-evaluated on the fly, data bindings to the model are refreshed automatically. The compile-time model contract—enforced by native check—and the runtime binder both re-execute during reload, guaranteeing type-safe updates. You can add a new bound attribute or rename a markup property and see the result instantly without restarting the app or resetting its state.
Simultaneous Web Frontend Hot-Reload
The CLI is not limited to native surfaces. When configured, native dev also starts a frontend dev server such as Next.js, Vite, React, Svelte, or Vue. This lets mixed native-WebView applications hot-reload both their .native markup and their web UI in parallel.
Programmatic Hot-Reload Configuration
You can enable the same behavior outside the CLI by configuring MarkupOptions directly.
Zig Usage
const ui = canvas.ui_markup.MarkupOptions{
.source = @embedFile("src/app.native"),
.watch_path = "src/app.native", // <-- enables hot reload
.io = std.io.getStdOut(),
};
var app = try UiApp.init(allocator, ui);
defer app.deinit();
try app.run(); // while running, edits to src/app.native are reloaded
TypeScript Core Usage
import { native } from "native-sdk";
await native.dev({
markup: { source: await readFile("src/app.native"), watchPath: "src/app.native" },
});
Summary
native devwatches.nativemarkup via thewatch_pathfield declared insrc/tooling/templates.zigandsrc/app_runner/ts_core_main.zig.- The runtime at
src/runtime/ui_app.zigcallsreadMarkupFileon change and rebuilds only the UI tree. - Provenance (
self.provenance.watching) preserves logical widget identities across reloads. - Application state survives because the model lives in Zig/TypeScript code, not in the markup layer.
- Model bindings refresh automatically through
native checkand the runtime binder. - Mixed native and web apps can hot-reload both surfaces concurrently.
Frequently Asked Questions
What file triggers hot reload in the Native SDK dev workflow?
The path defined by the watch_path field inside canvas.ui_markup.MarkupOptions, typically pointing to a .native file such as src/app.native. Both src/tooling/templates.zig and src/app_runner/ts_core_main.zig set this option for generated projects.
How does the runtime keep widget state intact during a reload?
The self.provenance.watching logic in src/runtime/ui_app.zig tracks stable widget identities across markup versions. By matching provenance IDs, the runtime reconciles the old and new trees instead of recreating widgets from scratch.
Why does application data survive when markup changes?
Application logic and state reside in the Zig or TypeScript core, completely separate from the declarative markup. Only the UI tree is rebuilt on reload; the underlying model, session fields, and network state remain in memory.
Can I hot reload both native markup and a web frontend at the same time?
Yes. The native dev CLI can concurrently launch a frontend dev server—such as Next.js, Vite, React, Svelte, or Vue—so mixed native-WebView projects receive live updates on both surfaces simultaneously.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →