# 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 (`[Route]`); 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
[Route("/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; }

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

[Route("/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}"); }
    }
  }
}
```

**A server function works too, as the whole initializer.** `live var n = PeopleCount(team);` is asked on mount and
asked again whenever `team` changes or data its last answer read is written ([[ui-reactivity#server-call]]). What
the compiler refuses is a server call INSIDE a larger expression (`live var label = "n=" + PeopleCount(team);`):
make the call the whole initializer, and compute from its answer in a second `live var`.

**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*. Four
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)` |
| an **element of a list this component owns**, or a **field of one** | `Cap[] caps = […];` → `DecimalField(caps[i].Label, value: caps[i].Amount)` |

#### Passing a binding on   {#passing-a-binding-on}

A `Binding<T>` prop is not a copy of the caller's value — it IS the caller's target. So a component can hand it on,
to a kit control or to another component of its own, and the write lands on the ORIGINAL owner however many
components it passes through. Nothing in between declares a field or an action to relay it.

```osy title="a page's field, bound through a component of the app's own to the kit's Switch" test app=ui-component-binding-pass-through
using Osysharp.Ui;

// Owns nothing: `showSteps` is the page's field, handed in and handed on.
[Composable]
component RecipeCard(string title, Binding<bool> showSteps) {
  render {
    Stack(gap: 2) {
      Text(title, fontWeight: FontWeight.Bold);
      Switch("Show the steps", value: showSteps);   // Switch declares `Binding<bool> value`
      if (showSteps) { Text("1. Warm the pot. 2. Add the leaves. 3. Wait four minutes."); }
    }
  }
}

[Route("/recipe")]
[AllowAnonymous]
[Render(CSR)]
component RecipePage() {
  bool showSteps = false;
  render {
    Stack(gap: 2) {
      RecipeCard("Green tea", showSteps);
      Text(showSteps ? "steps shown" : "steps hidden");   // the page's own field moved
    }
  }
}
```

A list travels the same way: a `Binding<string[]> items` prop binds its elements (`Field(value: items[i])`) exactly as
the owner's own field would.

**A plain parameter is not a target.** `component RecipeCard(bool showSteps)` receives a VALUE, which belongs to the
caller — so binding it (`Switch(…, value: showSteps)`) or assigning it in an action is refused, naming the parameter
and the declaration that makes it one: *"'showSteps' is a read-only parameter … Declare it `Binding<bool> showSteps`"*.

#### Editing a list   {#editing-a-list}

The fourth row is what lets a page edit rows it cannot name one by one. The first three targets are all *named at
compile time*, so a list whose length belongs to the data — five budget dimensions, one filter per column, whatever
the user just added — had no editable spelling at all: a field per element is precisely what it cannot have.

Two rules, and both are the same rule one level apart:

- **The list must be an assignable field of this component.** A `live var` is computed, so a write into it would be
  dropped, and the compiler says so rather than rendering a box that takes typing and changes nothing.
- **The write replaces, it does not mutate.** The element is replaced, then the list is — so both the element and the
  field hold new references and everything reading either one repaints. You never write that; it is what the binding
  does.

The element may be a scalar (`string[] typed;` → `Field(value: typed[i])`) or a `class`. If it is an **entity** row,
bind one of its *fields* rather than the element: a row has an identity, so `orders[i].Name` writes through the row
itself and the page goes dirty exactly as `order.Name` does.

```osy title="five editable rows over a list the page owns" test app=ui-component-editable-list
using Osysharp.Ui;

class Cap { public string Label = ""; public decimal Amount = 0m; }

[Route("/caps")]
[AllowAnonymous]
[Render(CSR)]
component CapsPage() {
  Cap[] caps = [
    new Cap { Label = "Tokens", Amount = 100m },
    new Cap { Label = "Storage", Amount = 25m },
  ];

  live var total = caps.Sum(c => c.Amount);

  render {
    Stack(gap: 2) {
      foreach (var i in Enumerable.Range(0, caps.Length)) {
        DecimalField(caps[i].Label, value: caps[i].Amount, min: 0m);
      }
      Text($"total {total}");
    }
  }
}
```

**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>`.

**A nullable target needs a control that can be empty.** A `DateOnly?` binds to `DatePicker` (its `Binding<DateOnly?>`
shows a placeholder while empty), but an `int?` or a `decimal?` into a control declaring `Binding<int>` /
`Binding<decimal>` is refused naming the parameter: that control has no empty state, so a null would reach its render
and the page would fail in the browser. A `string?` is fine — a text box shows null as empty. The other direction is
always legal: a non-null `DateOnly` into `Binding<DateOnly?>`, since the control can hold everything the field can. 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;

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

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

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

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

### Can a handler's body be a block that reads the row?   {#block-handler}
Yes. A handler's body may be any statement C# allows there — an assignment, an increment, or a whole `{ … }` block —
and a block closes over the names around it exactly as the expression form does: the `foreach` row, and the
parameter of a slot template (`Lines(items) { l => … }`). Each button keeps the row it was BUILT for.

```osy title="a block handler reading a foreach row and a slot template's parameter" test app=ui-component-handler-block
using Osysharp.Ui;

class Track { public string Title = ""; }

[Composable]
component TrackList(List<Track> tracks) {
  render { Stack { foreach (var t in tracks) { Row(gap: 2) { Text(t.Title); Slot(t); } } } }
}

[Route("/tracks")]
[AllowAnonymous]
[Render(CSR)]
component Tracks() {
  List<Track> tracks = [new Track { Title = "Intro" }, new Track { Title = "Outro" }];
  Track? playing = null;
  int plays = 0;
  render {
    Stack(gap: 2) {
      TrackList(tracks) { t =>
        Button("Play", onPress: () => { playing = t; plays++; });     // the template's parameter
      }
      foreach (var t in tracks) {
        Button("Queue " + t.Title, onPress: () => { playing = t; });  // the loop's row
      }
      if (playing != null) { Text("Playing " + playing!.Title + " · " + plays); }
    }
  }
}
```

A value a block handler reads from the render is handed to it when the button is drawn, so it has to be something
that can be handed over: a scalar, an enum, a row, a class instance, or a list of one. Anything else is a compile
error that names the value and says to pass what the handler needs off it instead.

The lambda takes no parameters, and its body must **do** something. A handler that only computes a value 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. The body does not wait for the parent's action,
but a failure is not lost: when that action throws and does not catch it (a refused save, say), the page shows the
failure exactly as it would for a button bound to the action directly, conflict notice included.

An OPTIONAL callback is a nullable delegate with a `null` default, and it is called the way C# calls one:
`onMore?.Invoke(x)` runs it when the caller passed one and does nothing when it did not. `f.Invoke(x)` is the same
call as `f(x)`.

```osy title="an optional callback, called only when the caller passed one" test app=ui-component-handlers
[Composable] component Chip(string label, Action<string>? onRemove = null) {
  action Remove() { onRemove?.Invoke(label); }
  render { Pressable(onClick: Remove) { Text(label + " ×"); } }
}
```

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

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

[Route("/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
- [action vs. method](https://osysharp.com/reference/ui/action/) — what `action` gives you that a plain `method` doesn't (surprisingly little), and when to reach
  for each
- [store](https://osysharp.com/reference/ui/store/) — sharing state and actions ACROSS components, rather than within one
