Summary#
on mount { … } and on unmount { … } are component lifecycle hooks — the bookends of an instance's life. Each is
an auto-invoked action: the runtime calls it for you instead of a click, so each can do everything an action can —
assign a field, create rows with new Entity{ … }, call server functions. They belong to the on <event> family
alongside the reactive on change block.
on mountruns once, when the component first mounts, before its first render. Its effects are in place for the first paint.on unmountruns once, when the component goes away, after its children have torn down. Use it for a final flush or an explicit release.on frame (double dt)runs once per displayed frame, for as long as the component is mounted. It is the odd one out and the only phase that repeats — see running code every frame.
component OrgCreatePage() {
Organization draft; // no initializer — null until mount
on mount { draft = new Organization {}; } // seed a fresh draft to bind the form to
on unmount { Log.Information("editor closed"); } // a parting side-effect
render { Input(value: draft.Name); … }
}Signature#
on mount { <statements> } // runs once, at first mount, before the first render
on unmount { <statements> } // runs once, at teardown, after children tear down
on frame (double dt) { … } // runs once per displayed frame; `dt` is the seconds since the last one
T name; // a field with NO initializer defaults to null (a bare C# field)Both are anonymous (the on <event> family carries no name), and a component may declare at most one of each.
Description#
Use on mount for the setup a page needs the moment it opens, and on unmount for the teardown it owes when it leaves:
- Runs once, per instance.
on mountfires when a component instance first mounts;on unmountfires when that same instance is disposed. Switching to a retained tab and back is the same instance, so neither re-fires; a reload or reopening the page is a new instance, which mounts (and later unmounts) again. on mountis before the first paint. State it sets and rows it creates are reflected in the first render — no flash of an empty form.on unmountis after the children. Teardown runs deepest-first: a child region'son unmountfires before its parent's, and a page's own live queries and reactions are disposed automatically as its scope goes away — you never hand-unsubscribe.on unmountis for the extra teardown only your code knows about.- Full action powers. Read/write a field,
new Entity{ … }(into the page's overlay), call a server function. It's the same body a user action runs.
A companion: a field's initializer is optional (D64). Organization draft; declares the field with no value (null),
exactly like a C# field — so on mount (or an action) can fill it in later. This is what lets an edit form and a create
form look the same: the edit page loads its row into a server read, the create page makes one in on mount, and
both bind their inputs to that one entity.
Creating vs. editing — the same shape. Because a created row lives in the page's overlay just like an edited one, a
create page is dirty-tracked exactly like an edit page: a freshly-seeded, untouched draft is not dirty (closing the
tab discards nothing and prompts nothing), and it becomes dirty the moment you type. UnitOfWork.Commit() on a Save action
persists the draft.
What these are not (yet). The lifecycle family is on mount / on unmount / on frame; on show/activate and
on route-change are not built yet. Both run client-side (a server-rendered page paints without on mount, then
runs it on hydration). A block that reads a server-read field which hasn't loaded yet sees the loading state, the same
as an action would. These are known limits, not bugs.
Running code every frame — on frame (double dt)#
on frame (double dt) runs once per displayed frame — roughly sixty times a second — and is the only lifecycle
phase that repeats and the only one that takes a parameter. It is what a game loop, a simulation or a physics step
is written in, and it is the clock a Canvas is drawn on.
It is not a reaction. on change re-runs when a value it read changes; a frame body is driven by time and typically reads nothing that changes on its own, so wiring it as a reaction would either never re-run or spin.
on frame (double dt) {
elapsed = elapsed + dt; // `dt` is SECONDS since the previous frame
x = x + speed * dt; // scale by dt, and the motion is frame-rate independent
}Five things about it are worth knowing before you write one:
dtis required, and it is seconds. A body that does not scale by elapsed time runs at whatever speed the display happens to be — the defining bug of a hand-rolled loop, and invisible on the machine it was written on. You name the parameter;dtis the convention, not the contract.- It is clamped at 100ms. Return to a backgrounded tab, or hit a long pause, and the real gap can be seconds; an
unclamped
dtwould teleport everything through walls in one step. Past 100ms the loop runs slow rather than wrong — so a body cannot measure its own frame rate below 10fps fromdt. UseDateTime.UtcNowif you need the true elapsed time. - It pauses while the tab is hidden, and the first frame back is an ordinary one rather than one carrying the whole absence.
- It never re-enters. The next frame is requested only after the body returns, so a slow body slows the loop down instead of queueing copies of itself.
- A body that throws stops the loop, loudly, and logs the error with its stack. Sixty identical errors a second would bury the first one — the only report anybody could act on. The symptom is "it worked, then froze"; read the log.
It is client-side by construction — it is driven by the browser's frame clock — so a body that reaches a server function is refused at compile time. A round trip per frame is never what anyone meant.
Examples#
A create form, symmetric with its edit form — on mount seeds the draft; the inputs bind to it; Create commits:
entity Organization { string Name; string Slug; }
[Page("/org/new")]
[Render(CSR)]
component OrgCreatePage() {
Organization draft;
on mount { draft = new Organization {}; }
action Create() { UnitOfWork.Commit(); Navigation.Close("/org/new", true); }
render {
Stack(gap: 4) {
Input(value: draft.Name, placeholder: "Organization name");
Input(value: draft.Slug, placeholder: "team-slug");
Button("Create", onPress: Create);
}
}
}Both bookends on one page — set a title at open, log the close at teardown:
[Page("/report")]
[Render(CSR)]
component Report() {
string range = "";
on mount { range = "last-30-days"; } // prime a filter the moment the page opens
on unmount { Log.Information("report closed"); } // a parting side-effect, after any child region tears down
render { Text(range); }
}See also#
- component — the component these live in, and its other members (fields,
action, server reads). - on change — the reactive sibling: a block that re-runs whenever a value it read changes, not just once.
- The reactivity & lifecycle model — the whole execution model: declarations vs
on mountvson changevson unmount. - creating & saving data —
new Entity{ … }andUnitOfWork.Commit(), and the two-wayInput(value: entity.Field)binding. - routes and pages — binding the page to a route (a create page is just another route).