Skip to Content
Concepts

Concepts

A complete uicast document with six entries. It loads products, lists them with a quantity input, edits one in a dialog, and shows a total that stays up to date. Every concept on this page is in it.

Entriesthe LLM generates
[
  {
    "key": "card",
    "component": "Card",
    "loading": "scopes.root.busy",
    "seed": [
      { "set": "scopes.root.busy", "literal": false },
      {
        "set": "scopes.root.products",
        "expr": "listProducts()"
      }
    ],
    "children": ["title", "refresh", "rows", "total", "editor"]
  },
  {
    "key": "title",
    "component": "Heading",
    "props": {
      "expr": "({ text: scopes.root.products.length + ' products' })"
    }
  },
  {
    "key": "refresh",
    "component": "Button",
    "props": { "literal": { "label": "Refresh" } },
    "callbacks": {
      "onClick": [
        { "set": "scopes.root.busy", "literal": true },
        {
          "set": "scopes.root.products",
          "expr": "listProducts()"
        },
        { "set": "scopes.root.busy", "literal": false }
      ]
    }
  },
  {
    "key": "rows",
    "component": "ProductRow",
    "each": "scopes.root.products",
    "as": "product",
    "keyBy": "id",
    "props": {
      "expr": "({ name: scopes.product.name, price: scopes.product.price, qty: scopes.product.qty })"
    },
    "callbacks": {
      "onQtyChange": [
        { "set": "scopes.product.qty", "expr": "evt.value" },
        {
          "expr": "updateProduct({ id: scopes.product.id, qty: evt.value })"
        }
      ],
      "onEdit": [
        {
          "set": "scopes.root.draftName",
          "expr": "scopes.product.name"
        },
        {
          "set": "scopes.root.draftPrice",
          "expr": "scopes.product.price"
        },
        {
          "set": "scopes.root.editingId",
          "expr": "scopes.product.id"
        }
      ]
    }
  },
  {
    "key": "total",
    "component": "Heading",
    "props": {
      "expr": "({ text: '$' + scopes.root.products.reduce((sum, p) => sum + p.price * p.qty, 0) + ' total' })"
    }
  },
  {
    "key": "editor",
    "component": "EditDialog",
    "hidden": "!scopes.root.editingId",
    "seed": [
      { "set": "scopes.root.draftName", "literal": "" },
      { "set": "scopes.root.draftPrice", "literal": 0 }
    ],
    "props": {
      "expr": "({ name: scopes.root.draftName, price: scopes.root.draftPrice })"
    },
    "callbacks": {
      "onNameChange": [{ "set": "scopes.root.draftName", "expr": "evt.value" }],
      "onPriceChange": [{ "set": "scopes.root.draftPrice", "expr": "evt.value" }],
      "onSave": [
        {
          "confirm": "Apply these changes to the product?",
          "set": "scopes.root.products",
          "expr": "scopes.root.products.map(p => p.id === scopes.root.editingId ? { ...p, name: scopes.root.draftName, price: scopes.root.draftPrice } : p)"
        },
        {
          "expr": "updateProduct({ id: scopes.root.editingId, name: scopes.root.draftName, price: scopes.root.draftPrice })"
        },
        { "set": "scopes.root.editingId", "literal": null }
      ],
      "onCancel": [{ "set": "scopes.root.editingId", "literal": null }]
    }
  }
]
Result
How it's built— definition, implementation, prompt & wiring
Definitionsdef.tsyou provide
import z from "zod"; import { createComponentDefinition } from "@uicast/core"; // card/def.ts export const CardDef = createComponentDefinition({ name: "Card", description: "A bordered container.", }); // heading/def.ts export const HeadingDef = createComponentDefinition({ name: "Heading", description: "A section heading.", props: z.object({ text: z.string().meta({ description: "The heading text" }), }), }); // button/def.ts export const ButtonDef = createComponentDefinition({ name: "Button", description: "A click target.", props: z.object({ label: z.string().meta({ description: "Button text" }), }), callbacks: { onClick: z.null().meta({ description: "Fires when pressed" }), }, }); // product-row/def.ts export const ProductRowDef = createComponentDefinition({ name: "ProductRow", description: "One product: its name, unit price, and an editable quantity.", props: z.object({ name: z.string().meta({ description: "Product name" }), price: z.number().meta({ description: "Unit price, in dollars" }), qty: z.number().meta({ description: "Quantity in the cart" }), }), callbacks: { onQtyChange: z.object({ value: z.number().meta({ description: "The new quantity" }), }), onEdit: z.null().meta({ description: "Fires when the edit button is pressed" }), }, }); // edit-dialog/def.ts export const EditDialogDef = createComponentDefinition({ name: "EditDialog", description: "A modal editing a product's name and unit price.", props: z.object({ name: z.string().meta({ description: "The product name being edited" }), price: z.number().meta({ description: "The unit price being edited" }), }), callbacks: { onNameChange: z.object({ value: z.string().meta({ description: "The new name" }), }), onPriceChange: z.object({ value: z.number().meta({ description: "The new price" }), }), onSave: z.null().meta({ description: "Fires on Save" }), onCancel: z.null().meta({ description: "Fires on Cancel" }), }, });
Implementationsimpl.tsxyou provide
import { createComponentImplementation } from "@uicast/react"; import { Button as UIButton } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { ButtonDef, CardDef, EditDialogDef, HeadingDef, ProductRowDef } from "./def"; // card/impl.tsx export const CardImpl = createComponentImplementation({ def: CardDef, render: ({ children }, { loading }) => ( <Card className={loading ? "w-72 animate-pulse opacity-60" : "w-72"} aria-busy={loading || undefined}> <CardContent className="grid gap-2">{children}</CardContent> </Card> ), }); // heading/impl.tsx export const HeadingImpl = createComponentImplementation({ def: HeadingDef, render: ({ text }) => <div className="font-semibold">{text}</div>, }); // button/impl.tsx export const ButtonImpl = createComponentImplementation({ def: ButtonDef, render: ({ label, onClick }) => ( <UIButton variant="outline" size="sm" className="justify-self-start" onClick={() => onClick()}> {label} </UIButton> ), }); // product-row/impl.tsx export const ProductRowImpl = createComponentImplementation({ def: ProductRowDef, render: ({ name, price, qty, onQtyChange, onEdit }) => ( <div className="flex items-center gap-2 border-t pt-1 text-sm"> <span className="truncate">{name}</span> <UIButton variant="ghost" size="sm" className="mr-auto h-6 px-1.5" onClick={() => onEdit()}> ✎ </UIButton> <span className="w-12 text-right text-muted-foreground">${price} ×</span> <input type="number" min={0} value={qty} onChange={(e) => onQtyChange({ value: e.target.valueAsNumber || 0 })} className="w-14 rounded border bg-transparent px-1 py-0.5 text-right" /> </div> ), }); // edit-dialog/impl.tsx export const EditDialogImpl = createComponentImplementation({ def: EditDialogDef, render: ({ name, price, onNameChange, onPriceChange, onSave, onCancel }) => ( <div className="fixed inset-0 z-50 grid place-items-center bg-black/40"> <div className="grid w-64 gap-2 rounded-lg border bg-background p-4 shadow-lg"> <div className="font-semibold">Edit product</div> <input value={name} onChange={(e) => onNameChange({ value: e.target.value })} className="rounded border bg-transparent px-2 py-1 text-sm" /> <div className="flex items-center gap-1 text-sm"> <span className="text-muted-foreground">$</span> <input type="number" min={0} value={price} onChange={(e) => onPriceChange({ value: e.target.valueAsNumber || 0 })} className="flex-1 rounded border bg-transparent px-2 py-1" /> </div> <div className="flex justify-end gap-2"> <UIButton variant="outline" size="sm" onClick={() => onCancel()}> Cancel </UIButton> <UIButton size="sm" onClick={() => onSave()}> Save </UIButton> </div> </div> </div> ), });
Host functiontools.tsyou provide
import z from "zod"; import { standardTool } from "standard-tool"; const LATENCY_MS = 350; const PRODUCTS = [ { id: 7, name: "Filter Papers", price: 8, qty: 5 }, { id: 8, name: "Espresso Beans", price: 15, qty: 1 }, { id: 9, name: "Pour-Over Kettle", price: 23, qty: 4 }, { id: 10, name: "Ceramic Mug", price: 14, qty: 2 }, ]; // Edits are per-id overrides in localStorage, so they survive a reload. const KEY = "uicast-docs-products"; type Override = { name?: string; price?: number; qty?: number }; function readOverrides(): Record<string, Override> { try { return JSON.parse(localStorage.getItem(KEY) ?? "{}"); } catch { return {}; } } export const listProducts = standardTool({ name: "listProducts", description: "The three most recently stocked products.", outputSchema: z.array( z.object({ id: z.number().int().meta({ description: "Product id" }), name: z.string().meta({ description: "Product name" }), price: z.number().meta({ description: "Unit price, in dollars" }), qty: z.number().meta({ description: "Quantity in the cart" }), }), ), async execute() { await new Promise((resolve) => setTimeout(resolve, LATENCY_MS)); // The list rotates on every call, so a refetch shows it change. PRODUCTS.push(...PRODUCTS.splice(0, 1)); const overrides = readOverrides(); return PRODUCTS.slice(0, 3).map((product) => ({ ...product, ...overrides[product.id] })); }, }); export const updateProduct = standardTool({ name: "updateProduct", description: "Update one product's name, unit price, or quantity.", inputSchema: z.object({ id: z.number().int().meta({ description: "Product id" }), name: z.string().optional().meta({ description: "New product name" }), price: z.number().optional().meta({ description: "New unit price" }), qty: z.number().optional().meta({ description: "New quantity" }), }), async execute({ id, name, price, qty }) { const overrides = readOverrides(); overrides[id] = { ...overrides[id], ...(name !== undefined && { name }), ...(price !== undefined && { price }), ...(qty !== undefined && { qty }), }; try { localStorage.setItem(KEY, JSON.stringify(overrides)); } catch { // Storage unavailable (private mode) — the edit still lives in state. } }, });
Partial promptgenerated from definition
# Available Components

Card, Heading, Button, ProductRow, EditDialog

## Component Details

- Card — A bordered container.

- Heading — A section heading.
  Props:
    - text: string — The heading text

- Button — A click target.
  Props:
    - label: string — Button text
  Event handlers:
    - onClick() — Fires when pressed

- ProductRow — One product: its name, unit price, and an editable quantity.
  Props:
    - name: string — Product name
    - price: number — Unit price, in dollars
    - qty: number — Quantity in the cart
  Event handlers:
    - onQtyChange(evt)
      - value: number — The new quantity
    - onEdit() — Fires when the edit button is pressed

- EditDialog — A modal editing a product's name and unit price.
  Props:
    - name: string — The product name being edited
    - price: number — The unit price being edited
  Event handlers:
    - onNameChange(evt)
      - value: string — The new name
    - onPriceChange(evt)
      - value: number — The new price
    - onSave() — Fires on Save
    - onCancel() — Fires on Cancel

# Available Functions

listProducts, updateProduct

## Function Details

- listProducts() => {
    id: number /* Product id; integer */;
    name: string /* Product name */;
    price: number /* Unit price, in dollars */;
    qty: number /* Quantity in the cart */;
  }[]: The three most recently stocked products.
- updateProduct({
    id: number /* Product id; integer */;
    name?: string /* New product name */;
    price?: number /* New unit price */;
    qty?: number /* New quantity */;
  }) => unknown: Update one product's name, unit price, or quantity.
Rendererrenderer.tsxyou mount it
import type { ComponentEntry } from "@uicast/core"; import { Evaluator } from "@uicast/expr"; import { type ConfirmComponentProps, EntriesRenderer, RendererProvider } from "@uicast/react"; import { Button } from "@/components/ui/button"; import { ButtonImpl, CardImpl, EditDialogImpl, HeadingImpl, ProductRowImpl } from "./impl"; import { listProducts, updateProduct } from "./tools"; const implementations = [CardImpl, HeadingImpl, ButtonImpl, ProductRowImpl, EditDialogImpl]; const evaluator = new Evaluator({ functions: [listProducts, updateProduct] }); // Without it the engine falls back to window.confirm. const fallbackComponents = { confirm: ({ open, message, onConfirm, onCancel }: ConfirmComponentProps) => open ? ( <div className="fixed inset-0 z-[60] grid place-items-center bg-black/40"> <div className="grid w-64 gap-3 rounded-lg border bg-background p-4 shadow-lg"> <div className="text-sm">{message}</div> <div className="flex justify-end gap-2"> <Button variant="outline" size="sm" onClick={onCancel}> Cancel </Button> <Button size="sm" onClick={onConfirm}> Confirm </Button> </div> </div> </div> ) : null, }; export function Products({ entries }: { entries: ComponentEntry[] }) { return ( <RendererProvider implementations={implementations} evaluator={evaluator} fallbackComponents={fallbackComponents}> <EntriesRenderer entries={entries} /> </RendererProvider> ); }

The tree is built from keys

Nothing is nested in the file. card lists ["title","refresh","rows","total","editor"], so those five are its children:

card ─┬─ title ├─ refresh ├─ rows (repeats: one element per product) ├─ total └─ editor (hidden until a row's ✎ is pressed)

card is the root: no line lists it as a child. A parent must arrive before its children. Until a child’s line arrives, the parent’s skeleton fills its place.

One line, many elements: lists

The rows line has each, which makes it a list: it repeats once per item in the array.

{ "each": "scopes.root.products", "as": "product", "keyBy": "id" }
  • each — the array. It runs in the parent’s scopes, so it cannot read the item.
  • as — names the item’s scopes. scopes.product is the product itself; scopes.$product is its row, with index (0, 1, 2 …) and id.
  • keyBy — the field the row’s id comes from.

Three products in state, three elements from one entry:

scopes.productscopes.$product.indexrendered
{ id: 8, name: "Espresso Beans", price: 15, qty: 1 }0Espresso Beans · $15 × 1
{ id: 9, name: "Pour-Over Kettle", price: 23, qty: 4 }1Pour-Over Kettle · $23 × 4
{ id: 10, name: "Ceramic Mug", price: 14, qty: 2 }2Ceramic Mug · $14 × 2

State lives in scopes

State is kept in named scopes. This document has three:

  • scopes.root — one per app, shared by every element. It holds products, set by the seed on line 1.
  • scopes.product — one per row, named by the list’s as: the product itself.
  • scopes.$product — one per row too: its index, id and own state.

Scopes do not nest: scopes.product is not inside scopes.root. But an element can read every scope around it. In a list inside a list, it reads all of them:

{ "key": "orders", "component": "Card", "each": "scopes.root.orders", "as": "order", "children": ["lines"] } { "key": "lines", "component": "Typography", "each": "scopes.order.lines", "as": "line", "props": { "expr": "({ text: scopes.order.id + ' / ' + scopes.line.sku })" } }

So every as must be a new name: a list that reuses the name of a scope around it fails. See State & Scopes.

Editing a row updates the total

The $135 total sits outside the list and reads the array:

{ "key": "total", "component": "Heading", "props": { "expr": "({ text: '$' + scopes.root.products.reduce((sum, p) => sum + p.price * p.qty, 0) + ' total' })" } }

Each row’s quantity input writes to the product:

{ "onQtyChange": [{ "set": "scopes.product.qty", "expr": "evt.value" }] }

The write puts an edited copy of the product into scopes.root.products, so the total updates. The dialog sits outside the list, so its Save button has no scopes.product to write. It writes scopes.root.products.map(...) itself, which is what the row’s write does behind the scenes.

Both edits then call updateProduct(...), a host function that saves the change. Save’s first step has a confirm: if the user declines, that step and every step after it are skipped. The host’s fallbackComponents.confirm draws that dialog for every document.

Reading state subscribes to it

The rows list reads scopes.root.products in its each. Nothing declares this. The engine finds the read in the expression and subscribes the element to that field.

{ "key": "rows", "each": "scopes.root.products", "as": "product", "keyBy": "id" } // └── auto-subscribes to scopes.root.products

Refresh writes scopes.root.products, which wakes every reader: the rows rebuild and the N products count updates. The callback never names the elements it wakes. See Reactivity.

Expressions compute; set changes

props.expr, hidden, loading, each, and the expr of every seed or callbacks step hold one expression in a subset of JavaScript. Two rules.

An expression only computes. Only a step’s set writes state:

{ "expr": "scopes.root.count + 1" } // ✅ computes { "set": "scopes.root.count", "expr": "currentValue + 1" } // ✅ computes, then writes { "expr": "scopes.root.count = 1" } // ❌ rejected: assignment

The same state must give the same result. props, hidden, loading and each run again only when a scope field they read changes. The time is not a scope field: props showing Date.now() keep the time of their last run, and jump when an unrelated field changes. So the prompt has the model take the time once, in a step, and store it:

{ "props": { "expr": "({ text: 'Saved at ' + Date.now() })" } } // ⚠️ runs, but jumps whenever the props re-run { "set": "scopes.root.savedAt", "expr": "Date.now()" } // ✅ taken on save; props read scopes.root.savedAt

Nothing runs on its own

An event runs a callback, the page updates, and then it waits. Expressions only read state. Callbacks write state when an event fires, seeds write it once at mount, and nothing else writes. So one event does a limited amount of work, and the page never loops on its own.

What makes a document valid

Most render failures are one of these.

Exactly one root — the element whose key is in no children:

{"key":"a","component":"Card","children":["b"]} ✅ root {"key":"b","component":"Typography"} ✅ referenced once {"key":"c","component":"Typography"} ⚠️ second root — nobody references c

Several roots still render, side by side, but the prompt asks for exactly one.

A list inside a list needs a new as. A nested list that reuses its parent’s as fails as invalid-list:

{"key":"orders","each":"scopes.root.orders","as":"row", ...} {"key":"lines","each":"scopes.row.lines","as":"row", ...} ❌ inside it, scopes.row already exists

Lists side by side may share a name: an invoices list and a quotes list can each hold a list with "as": "line".

props, hidden, loading and each cannot call host functions. The call fails in the element’s error slot. Host functions run in seed and callbacks, where the engine waits for the result, so there is no await: listProducts(), never await listProducts().

Sending a key again updates the element. It is not an error: the new line replaces the old one. Edit turns work this way:

{"key":"title","component":"Heading","props":{"expr":"({ text: 'Products' })"}} {"key":"title","component":"Heading","props":{"expr":"({ text: 'All products' })"}}

Vocabulary

Words with a specific meaning in uicast. props is narrower than usual: it leaves out event handlers, which definitions and entries list separately as callbacks.

TermMeaning
uicast documentOne complete UI: a flat list of entries that form a tree. The root is the entry no other entry lists in children; nothing else marks it. Streamed as JSONLines, one entry per line.
entryOne line of the stream: the JSON for one node. What mounts from it is its element. See Entry fields.
listAn entry that repeats once per array item, marked by each. as names two scopes per item: scopes.<as> is the item itself, and scopes.$<as> its row, with index, id and the row’s own state.
scopeNamed state that elements read and subscribe to: scopes.root for the whole document, plus two per list item, the item and its row. Scopes stack but never nest. See State & Scopes.
expressionOne expression in a subset of JavaScript, run against the scopes. It is the only part of a document that computes. Run by @uicast/expr: a closed grammar, every read checked at run time, no new Function.
value sourceHow a value is produced: { "literal": … } verbatim, or { "expr": … } evaluated. See Value Sources.
stepOne item of a seed or a callback: a value source, optionally with a set path to write it to. A step that reads what another writes runs after it. See Entry fields.
definition / implementationThe two halves of a component: the def is what the model reads (name, description, and StandardSchemaV1 & StandardJSONSchemaV1 schemas), the impl is the React code that draws it. A registered set of pairs is a catalog. See def and impl.
host functionA function the host gives expressions to call, sync or async; the engine waits for the result either way. Each is a standard-tool — name, description, optional StandardSchemaV1 & StandardJSONSchemaV1 schemas, execute. See Host functions.
Last updated on