# The reactivity & lifecycle model

> How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are once-only lifecycle bodies, and `on change` is a tracked reaction. A change re-renders ONLY the slots that read it — never the whole page — and a region that leaves disposes its own live queries and reactions automatically.

<!-- id: ui-reactivity · area: ui · stability: stable · html: https://osysharp.com/reference/ui/reactivity/ -->

## Summary        {#summary}
An Osy# component is **reactive by construction**. You *declare* what its values are; the runtime keeps them current and
re-renders only what actually changed. There is no re-render call, no dependency array, no manual subscribe/unsubscribe.
This guide is the mental model behind [component](https://osysharp.com/reference/ui/component/)'s members — read it once and the rest of the UI reference falls
into place.

Four kinds of member make up the model:

| Member | Runs | For |
|---|---|---|
| `live var x = …` / a server read | **continuously** — a live value binding, recomputed whenever anything it reads changes | *what a value IS* |
| `var x = …` | **once**, at setup — then it holds whatever it was last assigned | *state you assign to* |
| `on mount { … }` | once, as the component first loads | one-time imperative **setup** |
| `on change { … }` | at mount, then on every tracked change | a **reaction** — push a value outside the component |
| `on unmount { … }` | once, at teardown (after children) | one-time **teardown** |

**"Anything it reads" includes anything a helper it calls reads — WITH ONE EXCEPTION, AND IT IS THE CLOCK.** A
`live var` that calls a method or a top-level function tracks the DATA that function read. It does **not** track a
clock read inside it, and the compiler refuses that shape rather than letting it go stale:

| Written | Recomputes when |
|---|---|
| `live var due = items.Where(i => i.At < DateTime.UtcNow).ToList();` | the clock ticks, or `items` changes |
| `live var due = items.Where(i => IsDue(i, DateTime.UtcNow)).ToList();` — the instant passed IN | the same |
| `live var due = items.Where(IsDue).ToList();` — `bool IsDue(Item i) => i.At < DateTime.UtcNow;` | ⛔ **REFUSED** |

> ⛔ **THIS TABLE SAID "the same" FOR THE THIRD ROW UNTIL 2026-09-01, AND THE COMPILER HAS NEVER AGREED.** Written
> exactly as that row showed, `osy validate` answers:
> *"`live due` reads the clock through `IsDue(…)`, and a clock read inside a function does NOT advance — it is
> evaluated once… Reactivity is decided where the read is WRITTEN, not where the function is called."*
> ⚑ **And that refusal's own `for more` pointer names THIS PAGE**, so a reader who followed it arrived at the
> sentence that had just been rejected. Measured on eval run 23 of `012`, which wrote the documented form, was
> refused, and spent four calls re-reading this page trying to reconcile the two.
> **Read the clock where the value lives, or pass the instant in.**

## Description    {#description}

### The lifecycle of an instance   {#lifecycle}
An instance runs this sequence, once:

1. **Setup — declarations initialize.** `var`/`live var` fields take their initial value; server reads *kick off* (their
   rows stream in). A `live var` is a value BINDING and stays current for the instance's whole life; a plain `var`
   is a value you HOLD — its initialiser runs once and it changes only when something assigns to it.

   "Once" means once the data it reads is there, as in C#, where a read finishes before the next line runs. A plain
   `var` initialised from a server read takes the loaded value, whether that read is the component's own or a
   parameter its caller worked out from one (`PlanPanel(plan, prices.Where(…).ToList())`). The only exception is a
   field that something assigned while the read was still loading: it keeps what was assigned.

   ⚠ That distinction is the one readers get wrong. A derived value written as a plain `var` looks right, renders
   right on the first paint, and then never moves — so reach for `live var` whenever the value is *computed from*
   something that can change, and plain `var` only for state you assign to yourself.
2. **`on mount` — once, as the component first loads.** One-time imperative setup: seed an editable `new Entity{}`
   ghost, run a sequence, prime some state. It runs beside the first reads, and what it sets locally is on screen in
   the first paint; the paint does not wait for a server call it makes. (Data *loading* is a declaration — a query —
   never `on mount`.)
3. **First render — the DOM is built once**, and each dynamic slot (a text expression, a prop, an `if` condition, a
   `foreach` source) gets its own tiny reactive binding to exactly the values it reads.
4. **Steady state — a change wakes only what read it.** When a value changes, only the slots and `on change` blocks that
   *read that value* re-run — **not the whole page**. A one-row edit patches one text node; an unrelated field elsewhere
   is untouched.
5. **`on unmount` — once, at teardown.** Runs when the instance goes away, *after* its children have torn down. The
   instance's own live queries and reactions are disposed automatically at the same moment.

### Declarations vs. `on mount` — the one distinction to internalize   {#declaration-vs-mount}
A **declaration** says what data *is* — a pure value binding that stays live:

```osy title="a declaration — recomputes when its sources change" syntax
live var open = orders.Where(o => o.Status == Status.Open);   // recomputes when orders (or the filter) change
```

`on mount` is one-time **imperative** work — a `new Entity{}` you intend to *edit* (a declaration's `new T{}` is a plain
object, not a committable ghost), a load-time fetch, a startup side effect:

```osy title="on mount — imperative setup that must happen once" syntax
on mount { draft = new Organization {}; }   // a committable ghost in the page overlay, seeded once
```

Reach for a declaration first; reach for `on mount` only when the work is imperative and must happen exactly once.

### What a `live var` may be   {#live-var}
Which one it is follows from its initializer:

| Initializer | What you get | Stays current because |
|---|---|---|
| an **entity read** — `Invoice.Where(…)`, `from Invoice where …` | a **reactive query** | it subscribes to data changes *and* refetches when its dependencies change |
| a **projected read** — `Folder.Select(f => new FolderNode { … })` | a **reactive query of values** | same subscription, but each row is a plain projected shape (see below) |
| an expression over **client values** — `draft?.Name ?? "…"`, `a + b` | a **tracked computed** | it recomputes synchronously whenever a value it read changes |
| a call to a **server function** — `Summary(chosen)` | a **live server call** | it is asked again whenever an argument changes, or data its last answer read is written |
| a **`stream<T>` call** — `Tail(path)`, `Ask(question)` ([yield — a function that produces results over time](https://osysharp.com/reference/function/yield/)) | a **live append-only list** | the server pushes each item; when an argument changes it asks again ([[function-yield#follows-arguments]]) |

#### A server function as a live value   {#server-call}
Write the call as the whole initializer. The function runs on the server, as the person viewing the page, and its answer
is the value:

```osy title="✓ an ordinary server function as a live value" test app=ui-reactivity-live-call
entity Sensor {
  [MaxLength(80)] string Name;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

entity Reading {
  Sensor Source;
  double Value;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

[AllowAnonymous]
string Summary(Sensor? sensor) {
  if (sensor == null) { return "pick a sensor"; }
  var readings = Reading.Where(r => r.Source == sensor).ToList();
  return sensor.Name + ": " + readings.Count + " readings";
}

[Route("/sensors")]
[AllowAnonymous]
[Render(CSR)]
component Sensors() {
  Sensor? chosen = null;
  live var sensors = Sensor.OrderBy(s => s.Name).ToList();
  live var summary = Summary(chosen);   // asked again when `chosen` changes, or a reading of it is written
  action Choose(Sensor s) { chosen = s; }
  render {
    Stack(gap: 2) {
      foreach (var s in sensors) { Button(s.Name, onPress: () => Choose(s)); }
      Text(summary);
    }
  }
}
```

It is asked again in two cases:

- **An argument changes.** `chosen` moves, so the page asks again with the new value. The previous answer stays on
  screen until the new one replaces it — a value that blanked while it was refreshed would make the page jump on every
  change — and `Live.Pending(summary)` is true meanwhile ([below](#server-call-state)). Only the answer to the latest
  argument is ever kept.
- **Data its last answer read is written** — by this page, another page, another person, on any server. The platform
  records what the function actually read on each run, and listens for exactly that. `Reading.Where(r => r.Source ==
  sensor)` listens for readings of *that* sensor, so a reading of another sensor does not ask again. An answer that
  comes back the same is not sent.

Neither asks while the page is **hidden** — a page an `Outlet(retain: N)` keeps mounted while the reader is on another
one. A change it would have asked for is remembered, and the page asks once when it is shown again, however many
changes it missed.

Three things the compiler checks:

- **The function may only read.** A function that creates, changes or deletes a row, raises an event or starts a
  workflow is refused: it would be asked again on every change, its own included. Call it from an action instead.
- **Something no commit announces.** When the function reads the clock (`DateTime.UtcNow`) or calls out over the
  network, nothing tells the page that the answer moved, so the compiler warns. Pass the time in as an argument and
  move it on a cadence (`on every (TimeSpan.FromMinutes(1)) { now = DateTime.UtcNow; }`).
- **A ticking clock as the argument.** `live var now = DateTime.UtcNow;` TICKS — so `live var x = F(now);` asks the
  server again on every tick, a request each time the clock moves. The compiler warns and names the three times that
  move only when the answer should: one read once (`DateTime now = DateTime.UtcNow;`), the day
  (`DateOnly.FromDateTime(DateTime.UtcNow)`), or one moved on a cadence as above.

The call must be the **whole** initializer. A server call inside a larger expression is refused, because the browser
cannot wait for the server halfway through an expression:

```osy title="✗ a server call inside a larger expression" syntax
live var label = "Now: " + Summary(chosen);   // ✗ — make the call the whole initializer
live var summary = Summary(chosen);           // ✓ and compute from it:
live var label = "Now: " + summary;           // ✓ a tracked computed over the answer
```

One larger shape is still a single ask, and is allowed: a **guard** — a condition whose one branch is a constant and
the other the call. The server evaluates the whole conditional, so with nothing chosen it answers the constant without
calling the function, and it asks again when anything the initializer reads changes:

```osy title="a guarded live call" syntax
live var verdict = chosen == null ? null : CheckInVerdict(chosen, area);   // ✓ null until a climber is chosen
live var kids = Session.CurrentUser == null ? [] : MyChildren();          // ✓ an empty list for a visitor
```

The constant is a literal — `null`, a number, text — or a collection of literals (`[]`, `new List<string>()`), and
the function may call other server functions; it is asked as one call.

When a read of one entity is all the function does, the inline query is simpler and just as live:
`live var readings = Reading.Where(r => r.Source == chosen)`. A **client-side** function is a tracked computed like
any other expression, so `live var greeting = Shout(name)` recomputes in the browser when `name` changes. When the
answer builds up over time instead of being replaced (an assistant's reply, a log), make the function a `stream<T>`
([yield — a function that produces results over time](https://osysharp.com/reference/function/yield/)).

#### While its answer is on its way — and when it fails   {#server-call-state}
A live server call is one of the page's **first reads**, exactly as a query is:

- **`on mount` runs once its answers are in.** Every live call the component declares has answered for the arguments
  it reads *now* before `on mount` runs — a call whose argument is another call's answer included (`settings` above is
  asked again once `account` arrives, and `on mount` waits for that second answer). So the obvious edit form, which
  copies a saved value into a draft in `on mount`, opens on the saved value.
- **The body waits for them too.** The `skeleton { }` shows until then ([skeleton](https://osysharp.com/reference/ui/skeleton/)); a component that declares
  none shows nothing in that moment. Either way the body never draws its empty state over an answer that has not
  arrived, and never reads one as `null`: `live var week = Cost();` is typed by its function, so a helper taking a
  non-null `CostReport` is handed the answer, not a stand-in. A body that reads `Live.Pending(x)` has said what to
  draw meanwhile, so it paints at once instead. A `[Layout]` waits the same way: its `Outlet` is part of its body,
  so the routed page is put into it once the layout's own live calls have answered.
- **A failure is not final.** When the server refuses or faults, the call is failed; a server fault or a dropped
  connection is asked again on a short back-off first (a few seconds in all), so a blip is a pause, not a failure. A
  later change to what the answer read asks again, and its answer replaces the failure — the page does not freeze on
  one bad moment.

The page can say each of these itself. `Live.Pending(x)` is true while `x`'s answer is on its way — before the first
answer, and while an argument change is being asked again. `Live.Failed(x)` is true when the last ask failed, and
`Live.Error(x)` says why (`null` otherwise). They read any `live var`: a query answers the same three questions, and a
value the browser computes is never pending. They belong in `render` or in a `live var`; to keep an action from running
early, gate its control — `Button("Save", onPress: Save, disabled: Live.Pending(settings))`.

```osy title="an edit form over a live call — what is stored, and what it is doing" test app=ui-reactivity-live-state
entity Account {
  [MaxLength(80)] string Name;
  security { allow read, update when IsAnonymous || IsAuthenticated; }
}

entity AccountSettings {
  Account Account;
  int MinimumBalanceCents;
  security { allow read, update when IsAnonymous || IsAuthenticated; }
}

[AllowAnonymous]
Account? CurrentAccount() { return Account.FirstOrDefault(); }

[AllowAnonymous]
AccountSettings? SettingsOrNull(Account account) {
  return AccountSettings.Where(s => s.Account == account).FirstOrDefault();
}

[Route("/settings")]
[AllowAnonymous]
[Render(CSR)]
component SettingsPage() {
  live var account = CurrentAccount();
  live var settings = account == null ? null : SettingsOrNull(account);
  int minBalance = 0;                    // a draft: nothing is written until Save

  on mount {                             // runs once `account` AND `settings` have answered
    if (settings != null) { minBalance = settings.MinimumBalanceCents; }
  }

  action Save() {
    if (account == null) { return; }
    var s = AccountSettings.Where(x => x.Account == account).First();
    s.MinimumBalanceCents = minBalance;
    UnitOfWork.Commit();
  }

  render {
    Stack(gap: 2) {
      if (Live.Failed(settings)) { Text("Could not load the settings: " + (Live.Error(settings) ?? "")); }
      NumberField("Minimum balance", value: minBalance);
      Button("Save", onPress: Save, disabled: Live.Pending(settings) || Live.Failed(settings));
    }
  }
  skeleton { Text("Loading the settings…"); }
}
```

**Often you need no draft at all — bind the form to the row.** A field bound to a row the page reads edits that row in
the page's unsaved changes; nothing is written until `UnitOfWork.Commit()`, and then only what was changed. There is no
copy to make, so there is no moment at which the copy can be wrong:

```osy title="no draft: the field edits the row, and Save commits it" syntax
live var settings = AccountSettings.Where(s => s.Account == account).FirstOrDefault();
action Save() { UnitOfWork.Commit(); }
render {
  if (settings != null) {
    NumberField("Minimum balance", value: settings.MinimumBalanceCents);   // edits the row itself
    Button("Save", onPress: Save);
  }
}
```

A row a live server call answered binds the same way (`live var settings = SettingsOrNull(account);`). Keep a draft
when the page must hold an edit apart from the row — to compare, to validate across fields before anything changes, or
to offer Cancel without touching the row.

#### A projected `live var` — a live list of a shape you declared   {#projected}
A reactive query may **project into a `class`** ([Select (projections)](https://osysharp.com/reference/query/select/)), exactly as a function return may — so a `live var`
can hold a live list of the shape you actually want to render, not the raw entity:

```osy syntax
live var nodes = Folder.Select(f => new FolderNode {   // a live list of FolderNode — reshaped, and reactive
  FolderId = f.Id,
  Name     = f.Name,
  ParentId = f.Parent?.Id ?? Guid.Empty
});
```

It subscribes to the **source** entity (`Folder`) just like an entity read, so it refreshes on commit — create or delete
a folder and the list updates itself, with nothing to reload. What differs is the rows: a projection has **no row
identity**, so each row is a plain **value** of your shape rather than a tracked entity. You read its fields (`node.Name`)
and pass it on; there is simply no per-row entity to edit or track through it — the whole result refreshes together when
the source data changes. Reach for it wherever you would return a projected `class` from a server read, but want the
result to stay live instead of being fetched once.

### Fine-grained updates — why "on change" is tracked, not "every render"   {#tracking}
`on change` is **dependency-tracked**: it subscribes to exactly the reactive values its body reads, and re-runs only when
one of those changes. That is the whole reason the name is `on change` and not "effect that runs every render" — a block
that re-ran on every render would be waste, and the name forbids that reading. Any reactive read counts, not only a
`live var`: a plain state field reassigned by an action wakes it too. See [on change](https://osysharp.com/reference/ui/on-change/).

### Can I hold editable state as class values?   {#class-values}
A [class](https://osysharp.com/reference/class/index/) is an in-memory shape with no row behind it, so a component field holding a `List<T>` of
them is an obvious way to carry working state — a set of selections, a draft split, a basket of lines. The question
that follows is *if I write to one of those objects, does the screen move?*, and **the answer has two halves that
point in opposite directions.** Reading one is fully reactive. Binding a control to one is refused. Design against
one half alone and you will either write a reassignment you did not need, or an edit that cannot compile.

**Reading a class field is tracked like any other read.** A render slot that reads `p.Weight` subscribes to that
field, and a write through the object — from an action, through the list, through any alias of it, since a class is
a reference type ([[class-index#reference]]) — wakes exactly the slots that read it and nothing else. It is the same
[fine-grained tracking](#tracking) as everything else on this page; a class value is not a blind spot in it.

⚠ **So you do NOT need `picks = picks.ToList();` after mutating an element.** That reassignment is a reflex carried
in from frameworks that diff by list identity, and here it buys nothing: the write already re-rendered, and
rebuilding the list only makes the runtime redo work it had done. Write in place.

```osy title="an in-place write to a class field re-renders — the list is never reassigned" test app=ui-reactivity
class Pick {
  public string Name = "";
  public int Weight = 1;
  public Pick(string name) { Name = name; }
}

[Route("/picks")]
[AllowAnonymous]
[Render(CSR)]
component Picks() {
  List<Pick> picks = [ new Pick("Ann"), new Pick("Bo") ];

  int Total() { int t = 0; foreach (var p in picks) { t = t + p.Weight; } return t; }
  action Bump(Pick p) { p.Weight = p.Weight + 1; }   // in place — nothing reassigns `picks`

  render {
    Stack(gap: 3) {
      foreach (var p in picks) {
        Row(gap: 2) { Text(p.Name); Text("w=" + p.Weight); Button("Bump " + p.Name, onPress: () => Bump(p)); }
      }
      Text("Total shares: " + Total());
    }
  }
}

[Test]
void mutating_a_class_field_in_a_list_rerenders() {
  Ui.Visit("/picks");
  Assert.Visible("Total shares: 2");
  Ui.Click("Bump Ann");
  Assert.Visible("Total shares: 3");   // the derived total moved, from one field written in place
}
```

**But a control's two-way `value:` cannot target a class field.** A two-way binding has to write *back* somewhere,
and a class value has no row behind it to write through — so rather than hand you a field that accepts typing and
saves nothing, the compiler refuses it:

```osy title="✗ what a class field cannot be — a two-way binding target" syntax
foreach (var p in picks) { NumberField("Weight", value: p.Weight); }   // ✗ refused, at compile time
```

```text title="what the compiler says when you try it anyway"
ERROR  RESOLVE_ERROR  UI: cannot two-way bind to `p.Weight` — `p` is a `class`, and a class value has no row behind
it to write through, so the edit would be read-only. A two-way target is an assignable field of this component, or a
field of an ENTITY. Hold the value in a component field and copy it into the class when you save it, or make the row
a real entity.
```

Take the first of those two ways out and the whole thing works, because of the half above: **bind an ordinary
component field, then copy it into the class on save** — the copy is an in-place write, so the derived total moves
with it.

```osy title="✓ bind a component field, copy it into the class on save" test app=ui-reactivity
[Route("/picks/edit")]
[AllowAnonymous]
[Render(CSR)]
component PickEditor() {
  List<Pick> picks = [ new Pick("Ann"), new Pick("Bo") ];

  int draft = 1;      // an assignable component field — this is what the control binds to
  Pick editing;       // which class value the draft is destined for

  int Total() { int t = 0; foreach (var p in picks) { t = t + p.Weight; } return t; }

  action Edit(Pick p) { editing = p; draft = p.Weight; }
  action Save() { editing.Weight = draft; editing = null; }   // the copy — an in-place write, so the screen moves

  render {
    Stack(gap: 3) {
      foreach (var p in picks) {
        Row(gap: 2) { Text(p.Name); Text("w=" + p.Weight); Button("Edit " + p.Name, onPress: () => Edit(p)); }
      }
      if (editing != null) {
        Row(gap: 2) { NumberField("Weight", value: draft); Button("Save", onPress: () => Save()); }
      }
      Text("Total shares: " + Total());
    }
  }
}

[Test]
void a_component_field_carries_the_edit_into_the_class() {
  Ui.Visit("/picks/edit");
  Assert.Visible("Total shares: 2");
  Ui.Click("Edit Ann");
  Ui.Fill("Weight", "4");
  Ui.Click("Save");
  Assert.Visible("Total shares: 5");
}
```

**So pick the shape by how the value is produced, not by how it is stored.** A class value is an excellent carrier
for anything **derived** — a computed balance, a settlement transfer, a running subtotal, a
[projected row](#projected) — because those are written by your own code and read by the render, which is exactly
the half that works. It is the wrong carrier for anything a person **edits directly through a control**: there, either
keep the edited scalar in a component field and copy it across as above, or make the row a real
[entity](https://osysharp.com/reference/entity/declaration/) and bind to that. Both are ordinary; neither needs the list reassigned.

### Automatic disposal — the ease of `live` without the leak   {#disposal}
Every reactive thing a region creates — a live query's subscription, an `on change` reaction, a child component — is
**owned by that region's scope**. When the region leaves (a `foreach` row drops, an `if` branch flips, the page closes),
its scope disposes and takes all of that down with it, children-first. You never write the un-subscribe; forgetting to
stop a live-ness is not a bug you can have here.

## Examples       {#examples}
All four kinds on one page — a declaration feeds the render *and* a reaction; `on mount` seeds; `on unmount` closes out:

```osy title="the-whole-model" test app=ui-reactivity
entity Organization { string Name; }

[Route("/org/new")]
[Render(CSR)]
component OrgCreate() {
  Organization draft;
  on mount { draft = new Organization {}; }          // once, before paint — seed the editable ghost

  live var tabName = draft?.Name ?? "New organization";   // a live declaration — recomputes as you type
  on change { Navigation.SetTitle(tabName); }        // a reaction — re-runs only when tabName changes
  on unmount { Log.Information("create form closed"); }    // once, at teardown

  render {
    Stack(gap: 4) { Input(value: draft.Name, placeholder: "Organization name"); }   // the text slot tracks draft.Name
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the member table this model underlies.
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount` / `on unmount` in full.
- [on change](https://osysharp.com/reference/ui/on-change/) — the tracked reaction, in full.
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — how a seeded `new Entity{}` ghost rides the page overlay and `UnitOfWork.Commit()`.
- [Classes](https://osysharp.com/reference/class/index/) — what a class value is, and why an edit through a list index sticks ([[class-index#reference]]).
