Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

on mount / on unmount

on mount { … } / on unmount { … } / on frame (double dt) { … } — run a block once when the component appears, once when it goes away, and once per displayed frame

`on mount { … }` runs a block ONCE, the first time a component appears, beside its first reads; `on unmount { … }` runs a block ONCE when it goes away — after its children tear down. Both have the full power of an action (set state, create data with `new Entity{}`, call a server function): `on mount` sets a page up, `on unmount` tears it down.

stable2 examples compiled by CIuiauthoringlifecycle

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 mount runs once, when the component first mounts, beside its first reads. What it sets is in the first paint when it finishes within the paint's brief wait (about 150 ms — see [[ui-skeleton#where-it-shows]]); a body that waits on the server lands when the server answers, the way the page's data does.
  • on unmount runs 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 mount fires when a component instance first mounts; on unmount fires 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 mount is part of the page's first load. State it sets and rows it creates locally are reflected in the first render — no flash of an empty form. A body that calls the server does not hold the page's first paint: the page is drawn, and what the call sets lands when it answers.
  • on mount runs once the page's reads are in. Its queries have answered, every live server call (live var settings = SettingsOrNull(account);) has answered for the arguments it reads now — a chain of them included — and a row page has its row. So copying a saved value into a draft here sees the saved value, never the draft's default ([[ui-reactivity#server-call-state]]). A call that fails does not hold it back: the body runs and sees no value, and Live.Failed(x) says why.
  • on unmount is after the children. Teardown runs deepest-first: a child region's on unmount fires 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 unmount is 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). 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:

  • dt is 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; dt is 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 dt would 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 from dt. Use DateTime.UtcNow if 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; }

[Route("/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:

[Route("/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 mount vs on change vs on unmount.
  • creating & saving data — new Entity{ … } and UnitOfWork.Commit(), and the two-way Input(value: entity.Field) binding.
  • routes and pages — binding the page to a route (a create page is just another route).

Related

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

on change

`on change { … }` is a reactive **side-effect**: the runtime re-runs it whenever a value it read changes, so it's how…

The reactivity & lifecycle model

How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are…

creating & saving data

A UI `action` creates, updates and deletes data by writing `new Entity { … }`, assigning fields, and calling…

routes and pages

How a component becomes a page: it declares a route with `[Route("/catalog/{slug}")]`, and navigating to a matching…

Canvas

A drawing surface, and the verbs that paint on it. Put a `Canvas` in a render block, call `Draw.*` from an `on frame`…