# component

> The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live` queries/computeds, actions, methods), and a declarative render tree. A page is a component bound to a route.

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

## Summary        {#summary}
A `component` is the ONE archetype for all UI: a bounded reactive unit with typed props, reactive members, and a
declarative render tree. A page is a component bound to a route (`[Page]`); a list row, a dialog, and the app
shell are all components. The compiler lowers a component to typed metadata; the client runtime fetches that tree
and renders it — apps never ship JavaScript.

## Signature      {#signature}
```osy syntax
[Page("/route/{param}")]        // optional: bind to a route (route params bind to same-named props)
[Render(SSR|SSG|ISR|CSR)]       // delivery mode; unrouted components inherit their host's
[AllowAnonymous]                // opt this route out of auth (public); routes require auth BY DEFAULT
[Authorize(Policy)]             // optional: narrow a (default-)protected route to a named policy (checked reference)
component Name(Type prop, …) {
  Type name = init;                // component field — a client SNAPSHOT (assignable; frozen until reassigned)
  var name = Entity.ToList();      // a server read, taken ONCE (a snapshot; not kept in sync)
  live var name = Entity.ToList(); // a reactive query — kept current via the change channel
  live var name = expr;            // a tracked computed — client-side; may call and allocate, never the server
  action name(params) { … }        // event handler (client interpreter)
  RetType name(params) => expr;    // plain helper method (or { block })
  render { … }                     // the declarative render tree
}
```

## Description    {#description}

### Where does a component keep its state?   {#members}
A component is a class, and its **fields are its reactive instance state** — so there is no `state`/`query`/`derived`
keyword to declare one. A `Type name = init;` (or `var name = …;`) written **directly in the component body, above
`render`** is a reactive field — the same place a C# class puts its fields; whether it
holds a **client value** or a **server read** is inferred from the initializer, not spelled: an initializer that reads
an entity set (`Order.ToList()`, `User.Single(…)`) is a server read; anything else — a scalar, a `new Entity{}` ghost,
a bare field, or a query over a *local* collection — is a client value. (Method-locals inside an `action`/method stay
transient, as in any class.)

**One reactive marker: `live`.** A `live var` **tracks** — it stays in sync with what it reads:
- `live var xs = Entity.ToList();` is a reactive **query** (a data-change signal refetches it);
- `live var y = <expr>;` is a tracked **computed** (it recomputes when its inputs change).

A **naked `var` is a snapshot** — it reads its value once and freezes. It is a *cut*: reactivity does not flow through
it. `var rows = xs.ToList();` copies `xs`'s current rows and never updates again, even as `xs` does; a refresh is a new
assignment, not an implicit update. So use `live var` when a value should stay current, and a plain `var` when you want
a fixed copy.

⚑ **Holding WHICH ROW an editor or a dialog is for is a field like any other** — `Album? editing;` — and it is the
question people arrive at this section already asking. It has its own answer below: [[#row-in-state]]. You do not
need an id, and you do not need per-row state inside the `foreach`: a component member is ONE thing, and the row
travels to the action as an argument.

| Member | Declares | Evaluates |
|---|---|---|
| `T name = init;` / `var name = init;` | A client **snapshot** field | Initializer at mount; assignable — a write re-renders. Frozen until reassigned. |
| `Type name;` | A field with no initializer | Defaults to null, assigned later (e.g. an `on mount` ghost seed). |
| `var name = Entity.ToList();` | A server read, **once** (a snapshot) | Runs on the server; rows land in the shared client store, then freeze. Exposes `.items / .total / .loading / .error / .hasMore`. |
| `live var name = Entity.ToList();` | A **reactive** server query | The same read, kept current — a data-change SIGNAL (never row values) triggers a refetch. |
| `live var name = Entity.Single(x => x.Slug == slug);` | A reactive query bound to a value | The predicate may reference the component's **props** (a route param) **and its own fields / computeds** — each captured value is sent to the server read, and the query **refetches when it changes**. So a search box keys a list off a filter field, and a `[Layout]` keys a lookup off a computed slug; the query rebinds in place with no remount. |
| `live var name = Entity.Where(x => x.Ref == row).ToList();` | A reactive query keyed on a **row** the component holds | A query may compare a reference against a row another member loaded (`i.Organization == org`), and may guard on the row (`org != null && …`, `org is not null`). Both bind the row's **id** — the one member every client-held row carries — so the query sends a scalar, and an unloaded row answers **no rows** rather than a refusal. Any other member of a captured row (`org.Name`) is not sent: lift it into its own `live var` and key on that. |
| `live var name = expr;` | A tracked **computed** value | Recomputed on the CLIENT when its inputs change. It may **call a helper and allocate** — `live var shares = Settle(people, pot);` is a computed, not a query. The one thing it may not do is reach the **server**; that is the query form above. Not assignable. |
| `action name(params) { … }` | An event handler | On the client interpreter, invoked by an event prop. |
| `on change { … }` | A reactive side-effect | A tracked reaction: re-runs when a value it READ changes, to push that value somewhere OUTSIDE the component (e.g. `on change { Navigation.SetTitle(org.Name); }`). It may not assign the component's own state — that's a compile error. See [on change](https://osysharp.com/reference/ui/on-change/). |
| `on mount { … }` / `on unmount { … }` | Lifecycle bodies | Auto-invoked ONCE — `on mount` before first paint (seed a draft, kick off a load), `on unmount` at teardown (a final flush). See [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/). |
| `RetType name(params) => expr;` / `{ … }` | A plain helper method | Like an action; an expression body is a one-return function. A PURE one is callable from a render expression (`Text(Subtotal())`) — see [Calling helpers from render](https://osysharp.com/reference/ui/render-calls/). |

**A query keyed on a row is written the way C# reads it.** The guard and the comparison are both identity questions
about `org`, and the compiler keys the read on `org.Id` for you; nothing has to be lifted by hand.

```osy title="a query guarded on, and keyed on, a row another member loaded" test app=ui-component-row-key
using Osysharp.Ui;

entity Organization { [Unique, MaxLength(60)] string Slug; [MaxLength(100)] string Name; }
entity Invitation { Organization Organization; [MaxLength(200)] string Email; }

[Page("/org/{slug}/invites")]
component InvitesPage(string slug) {
  live var org = Organization.Where(o => o.Slug == slug).FirstOrDefault();
  live var invitations = Invitation.Where(i => org != null && i.Organization == org).ToList();
  render { Stack(gap: 2) { foreach (var i in invitations) { Text(i.Email); } } }
}
```

**A `live var` computed may CALL, and it may ALLOCATE.** It is an ordinary client expression that happens to be
tracked, so the whole computation can live in one helper and be *named* rather than smeared through `render`. There
is no purity budget to spend: a method that builds a `new List<T>` and returns it is a perfectly ordinary
initializer.

```osy title="a `live var` computed calling a helper that allocates" test app=ui-component-live-computed
using Osysharp.Ui;

class Share { public string Who; public decimal Amount; }

entity Person {
  [Required, MaxLength(80)] string Name;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

[Page("/split")]
[AllowAnonymous]
[Render(CSR)]
component SplitPage() {
  live var people = Person.ToList();       // the reactive QUERY — a server read, kept current
  decimal pot = 90m;                       // an ordinary client field

  live var shares = Settle(people, pot);   // the tracked COMPUTED — it CALLS, and it ALLOCATES

  List<Share> Settle(List<Person> ps, decimal total) {
    var rows = new List<Share>();
    foreach (var p in ps) { rows.Add(new Share { Who = p.Name, Amount = total / ps.Count }); }
    return rows;
  }

  render {
    Stack {
      foreach (var s in shares) { Text($"{s.Who} owes {s.Amount}"); }
    }
  }
}
```

**The one line it cannot be is a SERVER call.** A `live var` is exactly two things — a reactive query (an entity
read, which subscribes to data changes) or a tracked computed over values the client already holds. A call that runs
on the server is neither, and the compiler says so by name: *"`live n` cannot be initialized from `PeopleCount(…)` —
that runs on the server."* Either fetch it once (`Type n; on mount { n = PeopleCount(); }`) or inline the query
(`live var n = SomeEntity.Where(…)`).

**Members share one namespace.** A component is a class, so no two of its members may share a name — a field and an
action collide just as two fields do, because both are reached as `this.name`. Declaring the same name twice is a
compile error naming both declarations, exactly as it is in C#:

```osy title="✗ a field and an action cannot share one name" syntax
component Editor() {
  int Save = 0;
  action Save() { }        // error: component 'Editor' already declares 'Save' — the field on line 2
}
```

Members that declare no name at all — `on mount`, `on unmount`, an unnamed `on change` — cannot collide, so a
component may have as many as it needs.

### What can an input write back into?   {#two-way}
Binding an input writes back through what you bound, so the target has to be something that can be *written*. Three
things are:

| target | example |
|---|---|
| an **assignable field of this component** | `string draft = "";` → `Field("Draft", value: draft)` |
| a **field of an ENTITY row** the page holds | `Field("Name", value: p.Name)` inside `foreach (var p in people)` |
| a **`Binding<T>` prop** of this component | `component WeightRow(Binding<decimal> weight)` → `NumberField(l, value: weight)` |

**And the target's type must be the `T` the control declares**, with one exception named below. A binding is
two-way: the control reads a `T` out of the target and writes a `T` back into it, so there is no conversion to
insert — a conversion would need an inverse, and one that has an inverse is the same type under another name.
`DatePicker` declares `Binding<DateOnly>`, so a `DateTime` field is a compile error, and so is widening an `int`
into a `Binding<decimal>`. The refusal names the control that binds the type you are holding (`DateTimePicker` for a
`DateTime`, `DecimalField` for a `decimal`) — call that one, or declare the field as the control's `T` and convert
wherever you SET it. A conversion written in the bind itself is not a two-way target and is refused for that instead.

#### The exception: a number in a text field   {#number-in-a-text-field}

**A number may be bound to a `Binding<string>`.** `Field` declares one, so `Field("Rate", value: rate)` over a
`decimal` is legal: the value is formatted into the box on the way out and parsed back into the member's declared
type on the way in, through that type's own exact parse. What you type is what the member holds — a number, not
text that looks like one — so `rate + 1` adds.

It is the only conversion a two-way bind admits, because a number is the only thing that answers both of the
questions a free-text box asks: *can a reader type this value*, and *does what they typed come back as the value
they meant*. A closed set fails the first — nothing stops someone typing a word that is not a member, and the write
is refused later with nothing on screen to say so — and a date fails the second, since the same day has several
spellings and the reader's culture picks one. Both stay compile errors.

```osy title="a decimal in a text field, and the control that fits it better" test app=ui-component-number-in-text-field
using Osysharp.Ui;

[Page("/rate")]
[AllowAnonymous]
[Render(CSR)]
component RatePage() {
  decimal rate = 0m;
  render {
    Stack {
      Field("Rate", value: rate);            // legal — formatted out, parsed back in
      DecimalField("Hourly rate", value: rate, min: 0m, prefix: "£");
      Text($"doubled: {rate * 2}");
    }
  }
}
```

**Reach for `NumberField` (whole) or `DecimalField` (money, a measurement, a rate) anyway.** They bind the number
directly and give the box what a text field cannot: steppers, a `min`/`max` the browser itself enforces, and the
numeric keyboard on a phone. `osy lint` says so as a SHOULD-tier finding
(`ui-text-field-bound-to-a-number`) and names which of the two fits your type. Text that is not a number at all
lands the member on the same value an empty box does, which is what those controls do with it too.

Two things are **not** targets at all, and both are compile errors rather than a silently read-only box:

- **A field of a `class` value.** A `class` is an in-memory shape with no row behind it, so there is nothing to write
  through — *"cannot two-way bind to `k.Weight` — `k` is a `class` … the edit would be read-only."* Hold the value in
  a component field and copy it into the class when you save, or make the row a real `entity`.
- **A `live var`.** It is computed, so *"there is nothing to write back into."*

This decides a data model, not a line of markup: if a page must let a person EDIT the rows of a list, those rows are
an `entity`. See [[ui-data-mutation#edit-binding]] for the entity-field form, and [generic component](https://osysharp.com/reference/ui/generic-component/) for
`Binding<T>`.

### Imperative bodies — the receiver model   {#receiver}
`action` / `on change` / `on mount` / `on unmount` / method bodies resolve **function-style with the component as the
receiver** — the same
member-body mechanism `class` methods use ([class methods](https://osysharp.com/reference/class/methods/)):

- A bare member name is an implicit-`this` member: `count = count + 1` ≡ `this.count = this.count + 1`. Both
  spellings are legal (C# scoping); locals and parameters shadow members.
- **Only a client snapshot field is assignable.** Assigning a server read, a `live var` computed, or a prop is a
  compile diagnostic (a server read is a read-only handle; a `live var` computed is derived from its inputs).
- **No `await`.** A call to a server function inside an action is a plain call — `Login(email, password);` —
  and the runtime hands off by the callee's execution side, not by a keyword. (`await` exists only for
  `Workflow.Run`.)
- Declarative slots (field initializers, `live var` computeds, render expressions) instead see members as ambient
  names — the reactive scope the renderer evaluates.

### The render tree   {#render-tree}
Statements in `render { }` are declarative nodes, persisted as the component's typed render tree:

| Form | Meaning |
|---|---|
| `Stack(gap: 2) { … }` | A platform atom. The atom set is deliberately tiny (Stack, Box, Text, Button, Pressable, Image, Input, Link) — richer surfaces come from foreign controls, never new natives. |
| `Card(p.Name)` | A call to another component. Props bind positionally / by name; the child contributes its render OUTPUT (no wrapper element). |
| `Text(expr)` | Text content — any value expression over the component scope. |
| `if (…) { } else if (…) { } else { }` | A reactive conditional chain. |
| `foreach (var x in source) { }` | Iteration over a member/prop collection or an inline query. |
| `Button("Save", onPress: Save)` | An event prop binds an `action`/method by name: `onClick`, `onInput`, `onChange`, `onSubmit`. |
| `var n = rows.Count;` | An ordinary **local**, legal wherever a render statement is — including inside a control's or an atom's child block. |
| several statements at the TOP level | A `render` block takes **many siblings**: `render { Text("a"); Text("b"); }` compiles. A `Stack`/`Box` is for LAYOUT, never to make the tree well-formed. |

**A control's content block IS a render block** — same grammar, same locals, all the way down:

```osy title="a control's child block is a render block — and render takes many siblings" test app=ui-render-block
entity Kiln { string Name; }

component Card(string title) {
  render { Stack(gap: 1) { Text(title); Slot; } }
}

[Page("/kilns")]
[Render(CSR)]
component Kilns() {
  var kilns = Kiln.ToList();

  render {
    Text("Kilns");               // MULTIPLE top-level siblings — legal, no wrapper needed
    Text("—");
    Card("In the house") {        // a control's CONTENT BLOCK is a render block…
      var n = kilns.Count;       // …so a local, an `if` and a `foreach` are all legal inside it
      Text($"{n} kilns");
      if (n == 0) { Text("none yet"); }
      foreach (var p in kilns) { Text(p.Name); }
    }
  }
}
```

### What ships to the client, and what stays on the server?   {#delivery}
- The component TREE ships to the client; expression slots ride the same wire union as function bodies — one
  expression currency, one interpreter.
- A server-read field ships only its persisted root id; the client reads the rows from the server, and a live
  refetch reads them the same way.
- A routed component is public only when it declares `[AllowAnonymous]` — a component takes **no
  `public`/`internal` modifier** (writing one is an error). Who may reach a component is an authorization question
  (`[AllowAnonymous]`, `[Composable]`, `[Authorize]`), not a [type-visibility](https://osysharp.com/reference/types/visibility/) one, so the
  type has no visibility axis to set.
- Actions run on the client against component state; entity writes go through the optimistic overlay and commit on
  the server.

### Holding ONE row in state — `Album? editing;`   {#row-in-state}
A field may hold a **single entity**, not only a list — `Album? editing;` is the ordinary way to say *which row the
dialog is for*. It is a client value like any other field (a bare field, not a server read), it compares with `==`,
and `null` is the honest spelling of "nothing selected".

⚑ **You do not need an id.** Holding `Guid editingId;` and looking the row up again on every render is the shape
people reach for when they are unsure this is allowed — it is more code, it re-finds a row you already had, and it
loses the type. Hold the row.

```osy title="which row the editor is for" test app=ui-component-row-in-state
entity Album {
  [Required, MaxLength(120)] string Title;
  security { allow read, create, update when IsAnonymous; }
}

[Page("/albums")]
[AllowAnonymous]
component Albums() {
  live var albums = Album.OrderBy(b => b.Title);

  Album? editing;                                  // one row, or none
  string draftTitle = "";

  action Edit(Album b) { editing = b; draftTitle = b.Title; }
  action Cancel() { editing = null; }

  render {
    Stack {
      foreach (var b in albums) {
        Stack(role: UiRole.Group) {
          Text(b.Title);
          Button("Edit", onPress: () => Edit(b));
        }
      }
      if (editing != null) {
        Stack(role: UiRole.Group, label: "Edit album") {
          Input(value: draftTitle, label: "Title");
          Button("Cancel", onPress: Cancel);
        }
      }
    }
  }
}
```

⚠ **A field per ROW is the thing that does not work** — `foreach` renders one component body many times over, so a
single `borrowerName` field is shared by every row and typing in one types in all of them. Two shapes are right, and
neither needs per-row state: open ONE editor at a time against a held row (above), or give the row its own
**component**, whose fields are then genuinely its own.

## Binding a click to an action — event handlers   {#handlers}
An event prop takes one of your component's **actions**. Two forms:

```osy title="naming an action, and binding one with an argument" test app=ui-component-handlers
[Principal] entity User { [Required] string Email; }

entity Item {
  [Required] string Label;
  security { allow create, read, update, delete when IsAuthenticated; }
}

[Page("/toolbar")] [Render(CSR)]
component Toolbar() {
  var rows = Item.OrderBy(r => r.Label).ToList();
  action Save() { UnitOfWork.Commit(); }
  action Remove(Guid id) { var r = rows.Single(x => x.Id == id); r.Delete(); }   // over the rows already fetched

  render {
    Row {
      Pressable(onClick: Save) { Text("Save"); }                 // no arguments — name the action
      foreach (var row in rows) {
        Pressable(onClick: () => Remove(row.Id)) { Text("×"); }  // pass an argument
      }
    }
  }
}
```

`() => Remove(row.Id)` reads like C# and behaves like it: the argument is captured **where the handler is written**.
Inside a `foreach`, each row's button carries that row's id — the × removes the row you clicked, not the last one
drawn.

The lambda takes no parameters, and its body must **call** something. A handler that doesn't call anything would do
nothing, so it's a compile error rather than a button that silently ignores you:

```osy title="✗ a handler body must CALL something" syntax
Pressable(onClick: () => count)          // error: a handler's body must CALL an action or method
Pressable(onClick: () => Remove())       // error: 'Remove' takes 1 argument(s) but 0 were given
Pressable(onClick: () => delete(row.Id)) // error: 'delete' is not an action, method, or callback parameter
```

A component that takes a **callback parameter** can forward it the same way, which is how a shared component
(a tab, a table row) reports back what happened to it. A callback parameter is spelled as a C# delegate:
`Action` for a no-argument callback, `Action<T…>` for one that carries values (`Action<string> onClose`), and
`Func<T…, TResult>` for an accessor that returns a value (`Func<Row, bool> predicate`).

```osy title="a callback parameter" test app=ui-component-handlers
[Composable] component Tab(string path, Action<string> onClose) {
  render { Pressable(onClick: () => onClose(path)) { Text("×"); } }
}
```

A callback parameter can also be **invoked from an imperative body** — an `action`, `method`, or `on change` block — not
only from a render lambda. This is what lets a component do its own work first and *then* report back. A menu that
closes itself (a state write, which needs a body) before telling its parent where to go:

```osy title="a callback with two arguments" test app=ui-component-handlers
[Composable] component Switcher(string path, Action<string, bool> onLeave) {
  bool open = false;
  action Pick(bool dirty) {
    open = false;          // close the menu…
    onLeave(path, dirty);  // …then hand the destination to the parent
  }
  render { Pressable(onClick: () => Pick(true)) { Text(path); } }
}
```

The call is checked against the delegate's parameters exactly like the render-lambda form (`onLeave(path)` alone
would be a "takes 2 argument(s)" error). A callback is fire-and-forget from a body: it hands control to the parent's
action and evaluates to nothing, so it is a statement, not a value.

### How does a caller wire up a callback?   {#composing-a-callback}
The caller supplies a callback parameter by **naming an action** — `onPick: Pick` — the same way a render lambda
names one for `onClick`. This is what makes a shared row reusable, over any element type (a `string`, an entity,
whatever the list holds): one component, composed once per item, each instance carrying its own row and reporting
back through the same callback.

```osy title="a factored row, composed once per item" test app=ui-component-handlers
[Composable] component ItemRow(Item item, Action<Item> onPick) {
  render { Pressable(onClick: () => onPick(item)) { Text(item.Label); } }
}

[Page("/items")] [Render(CSR)]
component ItemList() {
  live var items = Item.ToList();
  Item? picked = null;
  action Pick(Item item) { picked = item; }
  render {
    Stack {
      foreach (var item in items) { ItemRow(item, onPick: Pick); }
    }
  }
}
```

⚠ **The composition itself must name an action — an INLINE LAMBDA there is refused**, even though the render-lambda
one line up (`onClick: () => onPick(item)`, *inside* `ItemRow`) is exactly the shape this page opened with. The
difference is WHERE the callback is invoked from: `onPick` is called by `ItemRow`'s own `onClick`, so composing it
has to hand the platform's event system something it can call directly with the clicked row — a bound action, never
an arbitrary expression:

```osy title="✗ composing with an inline lambda instead of naming an action" syntax
foreach (var item in items) {
  ItemRow(item, onPick: it => { Pick(it); Extra(); });   // error: bind by naming an action, not a lambda
}
```

If the caller needs something `ItemRow` does not pass — which page, group or column the row sits under — that
context is the CALLER's own to carry, not something to close over: widen the delegate to
`Action<Context, Item>` and invoke it as `onPick(context, item)`, so the value travels with the clicked row
instead of being captured at composition.

## Examples       {#examples}

```osy title="catalog" test app=ui-catalog
entity Product { bool Active; string Name; }

component Card(string label) {
  render { Text(label); }
}

[Page("/catalog/{slug}")]
[Render(CSR)]
component Catalog(string slug) {
  int count = 0;
  live var label = slug;
  var products = Product.Where(p => p.Active).ToList();
  live var top = Product.OrderByDescending(p => p.Name).Take(3).ToList();

  action Increment() { count = count + 1; }
  int doubled(int x) => x * 2;

  render {
    Stack(gap: 2) {
      Text("Catalog");
      foreach (var p in products) { Card(p.Name); }
      if (count > 0) { Text("has"); } else { Text("none"); }
      Button("+", onPress: Increment);
    }
  }
}
```

## See also       {#see-also}
- [class methods](https://osysharp.com/reference/class/methods/) — the shared member-body mechanism (`this`, implicit members, visibility)
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — why a component, alone among top-level types, takes no `public`/`internal`
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — server-side waits (workflows), distinct from UI reactivity
