Component implementation
A component is a pair: a definition the model reads and an implementation React runs. The implementation decides what renders: a component from your design system, or plain markup.
import { createComponentImplementation } from "@uicast/react";
import { Badge } from "./badge";
import { BadgeDef } from "./def";
export const BadgeImpl = createComponentImplementation({
def: BadgeDef,
render: ({ text, children, variant }) => (
<Badge variant={variant}>{children ?? text}</Badge>
),
});createComponentImplementation returns the implementation to pass to
<RendererProvider>. An optional third field,
skeleton, draws the component while it waits for data or
children.
The render function
render gets two arguments. The first is one object, typed from the def, with
every prop, every callback and children side by side
({ text, variant, onClick, children }):
-
Each prop, already resolved: the engine evaluates the entry’s
propsand parses the result through the def’s schema, so every.default()is applied.{ "key": "total", "component": "Typography", "props": { "expr": "({ text: 'Open: ' + scopes.root.open })" } } // render receives { text: "Open: 3" } // …and is called again with "Open: 4" the moment something writes scopes.root.open -
Each callback, a
(payload) => Promise<void>under its key in the def’scallbacks, present even if the entry wired none. Calling one parses the payload through its schema and runs the entry’s callback steps with it asevt. -
children, the rendered child elements, present only when the entry has achildrenarray. It is never a prop (a def declaring one throws).
The second is the render context:
entry, the entry being rendered, unevaluated.loading, the entry’sloadingexpression, evaluated. What to show while it istrueis up to the implementation, such as a busy state.scopes, the scopes the entry’s expressions read:scopes.rootand, inside a list, the item’s two scopes. It is for debugging; state changes go through callbacks.
export const SearchInputImpl = createComponentImplementation({
def: SearchInputDef,
render: ({ value, onChange, onClear, onKeyDown }) => (
<div>
<Input
value={String(value ?? "")}
onChange={(e) => onChange({ value: e.target.value })}
onKeyDown={(e) => onKeyDown({ key: e.key, repeat: e.repeat })}
/>
<button onClick={() => onClear()}>clear</button>
</div>
),
});Build each payload from the React event by hand, with exactly the fields its schema declares.
Skeleton
An optional skeleton draws a component while something is missing. It gets
four props: reason, entry, knownProps and children. reason says what
is missing:
"streaming": a child entry has not arrived yet. The parent’s skeleton fills the slot, with the parent’sentry, since the child’s component is not known yet. It gets nochildren."seeding": the element’s ownseedis waiting on an async host function. The element and everything under it draw as skeletons, asDocumentSkeletondraws them: each skeleton getschildren(its children’s skeletons, ornull) and draws its own tag, a list draws three items, and an element under it withhiddenis left out. Whileinitloads, the whole document draws this way.
knownProps holds the props that need no evaluation, a literal or none,
checked by the definition with its defaults filled in, as render’s are. It is
absent when the props come from an expression, and in a child’s slot.
export const CardImpl = createComponentImplementation({
def: CardDef,
render: ({ title, children }) => <Card title={title}>{children}</Card>,
// With `children`, draw the Card itself; without, fill a slot inside a real one.
skeleton: ({ knownProps, children }) =>
children === undefined ? <Skeleton className="h-40" /> : <Card title={knownProps?.title}>{children}</Card>,
});// while getSales() is in flight, the panel draws as CardImpl's skeleton,
// titled "Sales" from its literal props, with BarChartImpl's inside
{ "key": "panel", "component": "Card", "props": { "literal": { "title": "Sales" } },
"seed": [{ "set": "scopes.root.sales", "expr": "getSales()" }], "children": ["chart"] }
// once the seed settles, the Card renders; a "chart" line
// that hasn't streamed in yet leaves a slot, filled by CardImpl's skeleton
// without children, since the gap is the parent's
{ "key": "chart", "component": "BarChart",
"props": { "expr": "({ data: scopes.root.sales, xKey: 'month', yKeys: ['total'] })" } }With no skeleton, an element with children draws theirs, and anything else
draws fallbackComponents.defaultSkeleton, or nothing.
Registration
Pass every implementation to <RendererProvider implementations={...}>; the
definitions go to the prompt. Adding a component means
updating both.
Props the document controls
A model wrote every prop your implementation gets. The evaluator checks expressions, not what your JSX does with their results. Follow three rules:
- Put URLs only in URL props. Declare the prop as a URL in the
definition (
z.url()), andurlPolicychecks it beforerenderruns. A plain string prop insrc,hreforposteris checked by nothing. - Never put document text where it can run.
dangerouslySetInnerHTML,<style>contents,srcDocandon*attributes can run script. A CSS string can leak data throughurl(...). A style object with fixed keys is fine:style={{ width: props.width }}. A whole CSS or HTML string from the document is not. - Send plain data from callbacks, never the event. Everything that goes into a scope must be JSON-shaped.
The reference catalog follows all three. The security model holds only if your implementations do too.