# 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, before first paint | 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.

   ⚠ 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, before the first paint.** One-time imperative setup: seed an editable `new Entity{}` ghost, run
   a sequence, prime some state. Because it runs before first render, what it sets is already on screen in the first
   paint. (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 — and why an ordinary server function isn't one   {#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 **`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 stream *is* the subscription — the server holds the connection open and pushes, so there is nothing to poll and nothing to invalidate |

A call to an ordinary **server** function is none of these, so it is a compile error:

```osy title="✗ a live var cannot call a server function" syntax
live var files = FilesInFolder(selectedId);   // ✗ FilesInFolder runs on the server
```

Nothing subscribes such a value to data changes, and a tracked computed cannot recompute synchronously — it would have
to hand off to the server mid-render. Both ways out are spelled out by the diagnostic:

```osy title="the two ways out — fetch once, or inline the query" syntax
var files;                                            // ✓ fetch once, imperatively
on mount { files = FilesInFolder(selectedId); }

live var files = FileAsset.Where(f => f.FolderId == selectedId);   // ✓ inline the query — genuinely reactive
```

And a third, when the answer *builds up* rather than changing — make the function a `stream<T>` ([yield — a function that produces results over time](https://osysharp.com/reference/function/yield/)).
That removes the objection rather than working around it: a stream is its own subscription, so each result renders the
moment it arrives.

```osy title="a third way — make the producer a stream" syntax
stream<FileInfo> FilesInFolder(Guid id) { … yield return file; … }   // the producer
live var files = FilesInFolder(selectedId);                          // ✓ items appear as they are found
```

Inlining the query is almost always what you actually wanted: it re-runs when `selectedId` changes *and* when the
underlying rows change, which is the behaviour the server-function spelling only appeared to offer.

A **client-side** function is fine — it is a tracked computed like any other expression, so `live var greeting =
Shout(name)` compiles and recomputes when `name` changes. The rule keys on where the callee *runs*, not on the fact
that it is a call.

#### 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; }
}

[Page("/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
[Page("/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; }

[Page("/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]]).
