--- status: draft last_updated: 2026-05-18 --- # ADR-004: `key` as a First-Class Field on `UElement` **Status**: Proposed ## Context The reconciler needs an identity mechanism to match old children to new children across re-renders. Without identity, the reconciler can only do positional comparison, which produces incorrect results for reorderings (destroying and recreating instances instead of moving them). React uses the `key` prop for this, but putting `key` inside `props` causes problems: 1. **`key` leaks into component props** — React strips `key` before passing props to components, but developers still try to read `this.props.key`, which is `undefined`. The mental model is "it's a prop but not really a prop." 2. **`key` doesn't validate with component schemas** — If a component's TypeBox schema is `Type.Object({ name: Type.String() })`, passing `key` alongside `name` would fail validation because `key` isn't in the schema. 3. **`key` is a reconciler concern, not a component concern** — Components should never need to know their own key. It's metadata for the reconciliation algorithm, not data for the component function. ## Alternatives Considered - **`key` in `props` (React pattern)**: Store key as a regular prop, strip it in `h()` before passing to components. Rejected because TypeBox schema validation would fail on elements that include `key` without declaring it, and the "strip before passing" pattern is error-prone. - **Separate `keyMap` on `UElement`**: Store keys in a separate `Map` mapping keys to child indices. Rejected because it adds complexity for no benefit over a simpler field — the key is a property of each child, not a property of the parent. - **No key system (positional reconciliation only)**: Match children by position. Rejected because it produces incorrect results for reorderings — positional matching destroys and recreates instances instead of moving them. ## Decision Add `key` as a first-class optional field on `UElement`: ```typescript export type UElement = { type: string; props: UniversalProps; children: UNode[]; key?: string; // new field }; ``` The `h()` factory extracts `key` from the props argument and promotes it to the element level. `key` is never passed to component functions. ```typescript // h() extracts key: const { key, ...rest } = props ?? {}; return { type, props: rest, children, key: key as string | undefined }; ``` `URoot` does not get a `key` field. Roots are unique per `createRoot()` call and are never children of another element. `URoot.props.id` serves the identification purpose for roots. The TypeBox schema uses `Type.Optional(Type.String())` for `key`, so validation passes both with and without the field. ## Consequences ### Positive - **Clean separation** — `key` is metadata for the reconciler, not data for components. No "strip before passing" step needed. - **Schema-compatible** — `key` is outside `props`, so component schemas don't need to declare it. - **Type-safe** — `key?: string` is explicit in the TypeScript type, not hidden in a union prop type. - **Backward compatible** — elements without `key` continue to work with positional matching. The field is optional in both TypeScript and TypeBox. ### Negative - **`UElement` shape changes** — adding `key` is a non-breaking addition (optional field), but any code that spreads `UElement` into object literals will pick up `key: undefined` on unkeyed elements. - **Serialization includes `key` when present** — JSON-serialized elements will include `"key": "..."` when provided, adding a field that wasn't there before. Consumers that validate serialized UElements against the old schema (without `key`) will need to update their schemas. ### Neutral - **`key` accepts `string` only, not `number`** — React accepts numbers and coerces to strings. For UJSX, `string` only is simpler and avoids implicit coercion. Users can wrap in `String()` if needed. - **Duplicate keys use last-wins** — if two siblings have the same key, the reconciler warns and uses the last occurrence. This matches React behavior. ## References - Key field design research: `docs/research/reconciler/00-KEY-FIELD-DESIGN.md` - Schema architecture: [schema.md](../schema.md) - Element factory: [element-factory.md](../element-factory.md) - Reconciler algorithm: [reconciler.md](../reconciler.md)