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'skey: 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 askey: 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, andkey:there is a compile error that says where it belongs. - A component that declares a parameter called
keykeeps C#'s meaning:key:is that parameter — so no caller can key that component at all.osy lintsays so at the declaration (ui-component-parameter-named-key, SHOULD): name the parameter for what it is the key OF (invitationKey,regionKey), and leavekey: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 holds | the call site | without key: |
|---|---|---|
a draft — string name = product.Name; | a detail panel over the selected row | the previous row's draft, shown under the new row |
something on mount loaded for its argument | a pane whose argument is a field the page changes | the old value's data, never reloaded |
| nothing but reads of its parameters | anything | nothing 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 stateWhy 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#
- component — fields, parameters and the instance a call site makes.
- on mount / on unmount —
on mountandon unmount, which a new key runs again. - Calling helpers from render — the
foreachandifarms a keyed call sits in.