Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

key: on a component call

Editor(row, key: row.Id) — a new key value is a new instance: its fields start over and on mount runs again

`key:` on a component call names the instance. While the key reads the same the instance stays and takes its new arguments; when it reads differently the old instance goes and a fresh one is built — its fields are initialised again and its `on mount` runs again. Inside a `foreach` the key is also the row's identity, so a re-sorted list moves each row's component with its state. It is Blazor's `@key` and React's `key`.

stable1 example compiled by CIuiauthoringstate

Summary#

A component's fields are initialised once, when its instance is made. A call site that later passes a different argument does not make a new instance — the parameter changes, and the fields keep what they had:

component NoteEditor(Note note) {
  string text = note.Text;          // read ONCE, when the instance is made
  render { Input(value: text); }
}

NoteEditor(selected);               // pick another note: `note` changes, `text` still holds the first note's draft
NoteEditor(selected, key: selected.Id);   // pick another note: a fresh NoteEditor, `text` is the new note's

key: says this is a different one now. A new key value disposes the instance and builds a fresh one.

Signature#

<Component>(<args>…, key: <value>)
  • <value> is any ONE value: a row (key: row, which is the same as key: row.Id), an id, a name, a number, an enum member. A list is refused — a key is one value the instance is known by.
  • It is read on every render, like any argument. Same value → same instance; different value → fresh instance.
  • It is legal on a component call only. An atom (Text, Stack, Input) holds no state of its own, so there is nothing for a key to reset, and key: there is a compile error that says where it belongs.
  • A component that declares a parameter called key keeps C#'s meaning: key: is that parameter — so no caller can key that component at all. osy lint says so at the declaration (ui-component-parameter-named-key, SHOULD): name the parameter for what it is the key OF (invitationKey, regionKey), and leave key: to the platform.

Description#

When does a component need a key?#

When it holds state seeded from a parameter, and the call site can be handed a different value while it stays on screen:

the component holdsthe call sitewithout key:
a draft — string name = product.Name;a detail panel over the selected rowthe previous row's draft, shown under the new row
something on mount loaded for its argumenta pane whose argument is a field the page changesthe old value's data, never reloaded
nothing but reads of its parametersanythingnothing goes wrong — a read of a parameter follows it

A component that only READS its parameters in render (or in a live var) needs no key: those reads follow the argument as it changes. The key is for state the component OWNS.

What does a new key do?#

Everything an instance owns starts over: its fields take their initialisers again, its on mount runs again, its on unmount runs for the instance that leaves, and its queries are the new instance's. Going BACK to an earlier key is a fresh instance too — a key names an instance, it is not a cache of the old ones.

Rows of a foreach#

A foreach over rows (entities) already keys each row by the row, so its components move with their rows when the list is re-sorted. A foreach over plain values — strings, numbers, records — keys its rows by POSITION, so a re-sort hands each row's component a different item while it keeps its state. Put the key on the component the row calls, and the key becomes the row's identity:

foreach (var tag in tags) { TagChip(tag, key: tag); }     // re-sort, insert, filter: each chip keeps its own state

Why is this not the default?#

C# and Blazor do not reset a component when its parameters change, and neither does React: an instance that took new arguments is the same instance. Resetting on every argument change would throw away state the reader is in the middle of — a half-typed comment in a card whose Count just went up. The key states exactly which argument is the instance's identity, and nothing else resets.

Examples#

A list of notes beside an editor for the selected one. The editor holds a draft seeded from its note, so the call site keys it by the note:

entity Note {
  [Required, MaxLength(200)] string Title;
  [MaxLength(2000)] string Body;
  security { allow read, create, update when IsAuthenticated; }
}

component NoteEditor(Note note) {
  string draft = note.Body;              // seeded ONCE per instance — hence the key at the call site
  bool saved = false;
  action Save() { note.Body = draft; UnitOfWork.Commit(); saved = true; }
  render {
    Stack(gap: 2) {
      Text(note.Title);
      Input(value: draft, placeholder: "Write something");
      Pressable(onClick: Save) { Text("Save"); }
      if (saved) { Text("Saved."); }
    }
  }
}

[Route("/notes")]
component NotesPage() {
  live var notes = Note.OrderBy(n => n.Title).ToList();
  Note? selected = null;
  action Pick(Note n) { selected = n; }
  render {
    Row(gap: 4) {
      Stack(gap: 1) {
        foreach (var n in notes) { Pressable(onClick: () => Pick(n)) { Text(n.Title); } }
      }
      if (selected != null) { NoteEditor(selected!, key: selected!.Id); }
    }
  }
}

See also#

Related

component

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

on mount / on unmount

`on mount { … }` runs a block ONCE, the first time a component appears, beside its first reads; `on unmount { … }` runs…

Calling helpers from render

A render expression may call a PURE client helper — a component `method`, a client class method, a top-level function —…