
X Card
- 40 installs
- 4.7k repo stars
- Updated August 4, 2026
- ant-design/x
x-card is a skill for building AI-driven UIs with @ant-design/x-card, the React implementation of the A2UI protocol that renders agent JSON command streams.
About
This skill covers @ant-design/x-card, the React implementation of the A2UI protocol that lets AI agents render interactive UIs from structured JSON command streams. A developer uses it to set up XCard.Box and XCard.Card, register component catalogs, bind data via JSON Pointer paths, and handle user actions sent back to the agent. It also documents A2UI v0.9 commands and streaming progressive rendering.
- Builds AI-driven UIs with @ant-design/x-card, the React implementation of the A2UI protocol
- Covers XCard.Box/XCard.Card, A2UI v0.9 commands, catalogs, data binding, and actions
- Enables agents to render rich interactive UIs from structured JSON command streams
X Card by the numbers
- 40 all-time installs (skills.sh)
- Ranked #8,244 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
x-card capabilities & compatibility
- Capabilities
- ant design x · use x chat
- Use cases
- frontend · ui design · orchestration
What x-card says it does
Data binding via JSON Pointer paths (RFC 6901)
npx skills add https://github.com/ant-design/x --skill x-cardAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 4.7k |
| Last updated | August 4, 2026 |
| Repository | ant-design/x ↗ |
What it does
Render agent-driven interactive UIs from A2UI JSON command streams with @ant-design/x-card.
Who is it for?
Developers rendering agent-generated interactive UIs from A2UI command streams in React.
Skip if: Chat bubble UI (use ant-design-x) or data providers/streaming SDK internals (use @ant-design/x-sdk).
When should I use this skill?
Building AI-driven UIs with @ant-design/x-card, using A2UI commands, catalogs, data binding, actions, or streaming patterns.
What you get
An XCard.Box/Card setup that renders A2UI v0.9 command streams with data binding and action handling.
- XCard.Box/Card setup
- component catalog registration
- action handlers
By the numbers
- A2UI v0.9 with 4 command types (createSurface, updateComponents, updateDataModel, deleteSurface)
- data binding via JSON Pointer (RFC 6901)
Files
🎯 Skill Positioning
This skill covers `@ant-design/x-card` — the React implementation of the A2UI protocol, enabling AI agents to dynamically render rich interactive UIs through structured JSON command streams.
It covers:
XCard.Box+XCard.Cardcomponent usage- A2UI v0.9 command types:
createSurface,updateComponents,updateDataModel,deleteSurface - Custom component registration and catalog management
- Data binding via JSON Pointer paths (RFC 6901)
- Action handling — sending user events back to the agent
- Streaming progressive rendering patterns
- v0.8 ↔ v0.9 protocol differences
Scope: v0.9 is the recommended protocol. v0.8 is supported for backward compatibility only — prefer v0.9 for all new work.
Table of Contents
- 📦 Package Overview
- 🗂️ Component Architecture
- 🚀 Quick Start Decision Guide
- 🛠 Recommended Workflow
- 🚨 Development Rules
- 🤝 Skill Collaboration
- 🔗 Reference Resources
📦 Package Overview
| Package | Responsibility |
|---|---|
@ant-design/x-card | React renderer for A2UI protocol — XCard.Box, XCard.Card, catalog APIs |
@ant-design/x | Chat UI components (Bubble, Sender, etc.) — not covered here |
@ant-design/x-sdk | Data providers, streaming — not covered here |
npm install @ant-design/x-cardExports:
import {
XCard,
registerCatalog,
loadCatalog,
validateComponent,
clearCatalogCache,
} from '@ant-design/x-card';
import type {
XAgentCommand_v0_9,
XAgentCommand_v0_8,
ActionPayload,
Catalog,
CatalogComponent,
} from '@ant-design/x-card';
// Subcomponents
XCard.Box; // Container: receives commands, owns catalog maps
XCard.Card; // Renderer: renders a single surface by id🗂️ Component Architecture
XCard.Box
├── owns: catalogMap, surfaceCatalogMap
├── dispatches: commands → all XCard.Card children
├── aggregates: onAction events from all Cards
└── XCard.Card (id="surface-a")
│ ├── owns: component tree, data model, commandVersion
│ └── resolves: data bindings, triggers actions
└── XCard.Card (id="surface-b")
└── ...XCard.Box Props
interface BoxProps {
commands?: (XAgentCommand_v0_9 | XAgentCommand_v0_8)[];
/** Component names must start with an uppercase letter (React component convention) */
components?: Record<string, React.ComponentType<any>>;
onAction?: (payload: ActionPayload) => void;
children?: React.ReactNode; // Should contain XCard.Card elements
}XCard.Card Props
interface CardProps {
id: string; // surfaceId to render
}ActionPayload
interface ActionPayload {
name: string; // from action.event.name
surfaceId: string; // which surface triggered it
/**
* Context passed by component, with path references automatically resolved.
*
* For action.event.context fields using { path: "xxx" } format:
* - X-Card automatically resolves them to { value: "actual_value" }
* - Other properties (like label) are preserved
*
* Example input config:
* { username: { path: "/form/username", label: "用户名" } }
*
* Example resolved context:
* { username: { value: "张三", label: "用户名" } }
*/
context: Record<string, any>;
}🚀 Quick Start Decision Guide
| If you need to... | Read first |
|---|---|
| Set up XCard.Box + XCard.Card | USAGE.md → Basic Setup |
| Send commands from agent to card | COMMANDS.md |
| Register a custom component catalog | CATALOG.md → Local Catalog |
| Bind component props to live data | DATA_BINDING.md |
| Handle user interactions / form submit | ACTIONS.md |
| Build a streaming progressive UI | USAGE.md → Streaming |
| Migrate from v0.8 to v0.9 | COMMANDS.md → v0.8 vs v0.9 |
| Look up full prop types | API.md |
🛠 Recommended Workflow
1. Define your catalog — register a local catalog or use the A2UI Basic Catalog URL. 2. Register custom components — pass them via XCard.Box components prop. 3. Create the React tree — wrap surfaces with XCard.Box, add XCard.Card per surface. 4. Feed commands — push XAgentCommand_v0_9[] into commands prop (typically from streaming agent response). 5. Handle actions — receive ActionPayload in onAction, update commands in response.
Minimal Working Example
import React, { useState } from 'react';
import { XCard, registerCatalog } from '@ant-design/x-card';
import type { XAgentCommand_v0_9, ActionPayload, Catalog } from '@ant-design/x-card';
// 1. Define and register local catalog
const myCatalog: Catalog = {
catalogId: 'local://my_catalog.json',
components: {
Text: {
type: 'object',
properties: { text: { type: 'string' }, variant: { type: 'string' } },
required: ['text'],
},
Button: {
type: 'object',
properties: { text: { type: 'string' }, action: {} },
required: ['text'],
},
},
};
registerCatalog(myCatalog);
// 2. Custom component implementations
const Text: React.FC<{ text: string; variant?: string }> = ({ text, variant }) => (
<p className={`text-${variant ?? 'body'}`}>{text}</p>
);
const Button: React.FC<{ text: string; onAction?: (ctx: any) => void; action?: any }> = ({
text,
onAction,
action,
}) => <button onClick={() => onAction?.(action?.event?.context ?? {})}>{text}</button>;
// 3. Build commands (from agent stream)
const commands: XAgentCommand_v0_9[] = [
{
version: 'v0.9',
createSurface: {
surfaceId: 'welcome',
catalogId: 'local://my_catalog.json',
},
},
{
version: 'v0.9',
updateComponents: {
surfaceId: 'welcome',
components: [
{ id: 'root', component: 'Column', children: ['title', 'btn'] },
{ id: 'title', component: 'Text', text: { path: '/user/name' }, variant: 'h1' },
{
id: 'btn',
component: 'Button',
text: 'Start',
action: { event: { name: 'start', context: {} } },
},
],
},
},
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'welcome',
path: '/user/name',
value: 'Alice',
},
},
];
// 4. Render
export default function App() {
const [cmdQueue, setCmdQueue] = useState<XAgentCommand_v0_9[]>(commands);
const handleAction = (payload: ActionPayload) => {
console.log('Action:', payload.name, payload.context);
// Append new commands based on agent response
setCmdQueue((prev) => [...prev /* new commands */]);
};
return (
<XCard.Box commands={cmdQueue} components={{ Text, Button }} onAction={handleAction}>
<XCard.Card id="welcome" />
</XCard.Box>
);
}🚨 Development Rules
- Always include `"version": "v0.9"` on every command — omitting it causes protocol rejection.
- One and only one `id: "root"` component per surface's component tree — this is the tree root.
- Flat adjacency list only — never nest component objects inside other component objects; always reference children by
idstring. - Separate structure from data —
updateComponentsfor layout,updateDataModelfor content/state. - Register catalog before mounting — call
registerCatalog()before the component tree renders. - Pass `components` map to `XCard.Box`, not to
XCard.Card— Box distributes to all Cards. - Never recreate the `components` object inline — keep it stable with
useMemoor module-level constant to avoid re-renders. - Input components require `value: { path: "..." }` for two-way binding — literal values do not update the data model.
- For streaming: append new commands to the array rather than replacing it — Card processes the diff incrementally.
- `action.event.context` paths are write targets — they point to where user-entered data lives in the data model; do not resolve them as read sources.
- Path references in action context are automatically resolved — when an action is triggered, X-Card converts
{ path: "xxx" }in the action config to{ value: "actual_value" }in the onAction payload. This works for both v0.9 (action.event.context = { key: { path } }) and v0.8 (action.context = [{ key, value: { path } }]) formats.
🤝 Skill Collaboration
| Scenario | Skill combination |
|---|---|
| AI chat with structured card responses | use-x-chat + x-components + x-card |
| Standalone agent form UI | x-card only |
| Streaming Markdown + card side-panel | x-markdown + x-card |
| HTTP streaming from agent into card | x-request → feed response as commands |
🔗 Reference Resources
- USAGE.md — Setup guide, streaming pattern, multi-surface examples
- COMMANDS.md — All four A2UI v0.9 command types, v0.8 vs v0.9 diff
- DATA_BINDING.md — JSON Pointer paths, dynamic types, two-way binding, template iteration
- ACTIONS.md — Action definitions, ActionPayload, form submission pattern
- CATALOG.md — Local catalog registration, remote URL loading, custom component schema
- API.md — Full TypeScript types for Box, Card, commands, catalog, actions
Official Documentation
Actions Reference
This file covers how user interactions flow from components back to the agent via onAction. Read this when implementing button handlers, form submission, or any user interaction.
---
Action Flow
Component (Button click)
→ Card resolves action.event.context paths
→ Card calls onAction(ActionPayload)
→ XCard.Box forwards to your onAction handler
→ Your code sends new commands to agent / updates cmdQueue---
ActionPayload
interface ActionPayload {
name: string; // from action.event.name
surfaceId: string; // which surface triggered this
/**
* Context passed by component, with path references automatically resolved.
* Path references in action.event.context are converted to { value } format.
*/
context: Record<string, any>;
}---
Path Reference Resolution
When a component triggers an action, X-Card automatically resolves path references in the context to actual values.
v0.9 Format
{
"id": "submit_btn",
"component": "Button",
"action": {
"event": {
"name": "submit_form",
"context": {
"username": { "path": "/form/username", "label": "用户名" },
"email": { "path": "/form/email", "label": "邮箱" }
}
}
}
}When the user clicks the button, onAction receives:
{
name: 'submit_form',
surfaceId: 'contact_form',
context: {
// Path references are automatically resolved to { value } format
username: { value: '张三', label: '用户名' },
email: { value: 'test@example.com', label: '邮箱' }
}
}v0.8 Format
In v0.8, action context uses array format:
{
"id": "submit_btn",
"component": {
"Button": {
"action": {
"name": "submit_form",
"context": [
{ "key": "username", "value": { "path": "/form/username" } },
{ "key": "email", "value": { "path": "/form/email" } }
]
}
}
}
}Resolved payload:
{
name: 'submit_form',
surfaceId: 'contact_form',
context: {
username: { value: '张三' },
email: { value: 'test@example.com' }
}
}Note: Only values that are{ path: "xxx" }format in the context are converted. Actual values passed by component (e.g.,{ value: "actual_value" }) are preserved without conversion.
---
Defining an Action on a Component
{
"id": "submit_btn",
"component": "Button",
"text": "Submit",
"action": {
"event": {
"name": "submit_form",
"context": {
"email": { "path": "/form/email" },
"name": { "path": "/form/name" },
"subscribe": { "path": "/form/subscribe" }
}
}
}
}When the user clicks the button, onAction receives:
{
name: 'submit_form',
surfaceId: 'contact_form',
context: {
// Path references are automatically resolved to { value } format
email: { value: 'alice@example.com' },
name: { value: 'Alice' },
subscribe: { value: true }
}
}---
Handling Actions in React
const handleAction = (payload: ActionPayload) => {
if (payload.name === 'submit_form') {
// Keys using { path } bindings in the config are resolved to { value, ...rest } format.
// Literal values from the config and runtime values from the component are preserved as-is.
const email = payload.context.email?.value;
const name = payload.context.name?.value;
// 1. Call your agent API
// 2. Push new commands in response
setCmdQueue(prev => [
...prev,
{
version: 'v0.9',
updateComponents: {
surfaceId: payload.surfaceId,
components: [
{ id: 'root', component: 'Text', text: `Thanks, ${name}! Confirmation sent to ${email}.` }
]
}
}
]);
}
};
<XCard.Box commands={cmdQueue} onAction={handleAction} components={...}>
<XCard.Card id="contact_form" />
</XCard.Box>---
Form Submission Pattern
Full round-trip with form validation:
// 1. Agent sends form structure
const formCommands: XAgentCommand_v0_9[] = [
{ version: 'v0.9', createSurface: { surfaceId: 'form', catalogId: 'local://cat.json' } },
{
version: 'v0.9',
updateComponents: {
surfaceId: 'form',
components: [
{ id: 'root', component: 'Column', children: ['email_input', 'submit_btn'] },
{
id: 'email_input',
component: 'TextField',
label: 'Email',
value: { path: '/form/email' },
checks: [
{
call: 'required',
args: { value: { path: '/form/email' } },
message: 'Email is required',
},
{
call: 'email',
args: { value: { path: '/form/email' } },
message: 'Invalid email format',
},
],
},
{
id: 'submit_btn',
component: 'Button',
text: 'Submit',
action: { event: { name: 'submit', context: { email: { path: '/form/email' } } } },
},
],
},
},
{ version: 'v0.9', updateDataModel: { surfaceId: 'form', path: '/form', value: { email: '' } } },
];
// 2. Handle submission
// Keys using { path } bindings in the config are resolved to { value, ...rest } format.
// Literal values from the config and runtime values from the component are preserved as-is.
const handleAction = async (payload: ActionPayload) => {
if (payload.name === 'submit') {
const email = payload.context.email?.value;
// Show loading
setCmdQueue((prev) => [
...prev,
{ version: 'v0.9', updateDataModel: { surfaceId: 'form', path: '/ui/loading', value: true } },
]);
// Process with agent, then show result
const result = await callAgent({ email });
setCmdQueue((prev) => [
...prev,
{
version: 'v0.9',
updateDataModel: { surfaceId: 'form', path: '/ui/loading', value: false },
},
{
version: 'v0.9',
updateComponents: {
surfaceId: 'form',
components: [{ id: 'root', component: 'Text', text: `Done: ${result}` }],
},
},
]);
}
};---
Client-Side Function Actions
For local-only actions (no server round-trip):
{
"id": "link_btn",
"component": "Button",
"text": "Open Docs",
"action": {
"functionCall": {
"call": "openUrl",
"args": { "url": "https://a2ui.org" }
}
}
}Available built-in functions: openUrl, formatString, formatNumber, formatDate, formatCurrency, pluralize, and, or, not.
---
Validation Checks on Buttons
A Button with checks auto-disables when conditions are not met:
{
"id": "submit_btn",
"component": "Button",
"text": "Submit",
"checks": [
{
"condition": {
"call": "and",
"args": {
"values": [
{ "call": "required", "args": { "value": { "path": "/form/name" } } },
{ "call": "email", "args": { "value": { "path": "/form/email" } } }
]
}
},
"message": "Name and valid email required"
}
],
"action": {
"event": {
"name": "submit",
"context": { "name": { "path": "/form/name" }, "email": { "path": "/form/email" } }
}
}
}⚠️ action.event.context paths are write targets pointing to where user input is stored. The Card resolves them by reading the data model when the action fires — do not mistake them for read bindings.API Reference
Full TypeScript types for @ant-design/x-card.
---
React Components
XCard.Box
interface BoxProps {
/** Command queue — append new commands; do NOT replace entire array on each update */
commands?: (XAgentCommand_v0_9 | XAgentCommand_v0_8)[];
/** Map of component name → React component implementation */
components?: Record<string, React.ComponentType<any>>;
/** Called when a surface component triggers an action */
onAction?: (payload: ActionPayload) => void;
children?: React.ReactNode;
}XCard.Card
interface CardProps {
/** The surfaceId this card renders */
id: string;
}---
Command Types (v0.9)
type XAgentCommand_v0_9 =
| { version: 'v0.9'; createSurface: CreateSurfacePayload }
| { version: 'v0.9'; updateComponents: UpdateComponentsPayload }
| { version: 'v0.9'; updateDataModel: UpdateDataModelPayload }
| { version: 'v0.9'; deleteSurface: DeleteSurfacePayload };
interface CreateSurfacePayload {
surfaceId: string;
catalogId: string;
theme?: { primaryColor?: string; iconUrl?: string; agentDisplayName?: string };
sendDataModel?: boolean;
}
interface UpdateComponentsPayload {
surfaceId: string;
components: BaseComponent_v0_9[];
}
interface BaseComponent_v0_9 {
id: string;
component: string;
child?: string;
children?: string[] | { path: string; componentId: string };
[key: string]: any | PathValue;
}
interface PathValue {
path: string;
}
interface UpdateDataModelPayload {
surfaceId: string;
path?: string; // JSON Pointer (RFC 6901). Default: "/" (full replace)
value?: any; // Omit to delete key at path
}
interface DeleteSurfacePayload {
surfaceId: string;
}---
Action Types
interface ActionPayload {
name: string;
surfaceId: string;
context: Record<string, any>;
}
// Server action definition (on component)
interface ServerAction {
event: {
name: string;
context: Record<string, any | PathValue>;
};
}
// Client-side function action
interface FunctionAction {
functionCall: {
call: string;
args: Record<string, any>;
};
}---
Catalog Types
interface Catalog {
$schema?: string;
$id?: string;
title?: string;
description?: string;
catalogId?: string;
components?: Record<string, CatalogComponent>;
functions?: Record<string, any>;
$defs?: Record<string, any>;
}
interface CatalogComponent {
type: 'object';
properties?: Record<string, any>;
required?: string[];
allOf?: any[];
[key: string]: any;
}---
Catalog API Functions
/** Register a local catalog (call before mounting) */
function registerCatalog(catalog: Catalog): void;
/** Load a catalog by ID — checks local registry first, then remote fetch */
function loadCatalog(catalogId: string): Promise<Catalog>;
/** Validate a component's props against a loaded catalog */
function validateComponent(
catalog: Catalog,
componentName: string,
componentProps: Record<string, any>,
): boolean;
/** Clear the in-memory catalog cache */
function clearCatalogCache(): void;---
v0.8 Types (deprecated)
type XAgentCommand_v0_8 =
| { updateComponents: UpdateComponents_v0_8 }
| { dataModelUpdate: DataModelUpdate_v0_8 };
interface UpdateComponents_v0_8 {
surfaceId: string;
catalogId: string;
components: ComponentWrapper_v0_8[];
}
interface ComponentWrapper_v0_8 {
id: string;
component: Record<string, Record<string, any>>; // { "Button": { props } }
}
interface DataModelUpdate_v0_8 {
surfaceId: string;
contents: Array<{
key: string;
valueString?: string;
valueMap?: Array<{ key: string; valueString: string }>;
}>;
}Catalog Reference
This file covers catalog registration, loading, and custom component schema definition. Read this when setting up a new catalog or authoring custom components.
---
What Is a Catalog?
A catalog is a JSON Schema file defining which component types an agent can use on a surface. Every createSurface command references a catalogId. The Card validates incoming component props against the catalog.
---
Local Catalog Registration
For local/custom catalogs, use the local:// URI convention and register before mounting:
import { registerCatalog } from '@ant-design/x-card';
import type { Catalog } from '@ant-design/x-card';
const myCatalog: Catalog = {
catalogId: 'local://my_catalog.json',
components: {
Text: {
type: 'object',
properties: {
text: { type: 'string' },
variant: { type: 'string', enum: ['h1', 'h2', 'body', 'caption'] },
},
required: ['text'],
},
Button: {
type: 'object',
properties: {
text: { type: 'string' },
variant: { type: 'string', enum: ['primary', 'borderless'] },
action: {},
checks: {},
},
required: ['text'],
},
TextField: {
type: 'object',
properties: {
label: { type: 'string' },
value: {}, // Supports { path: string } binding
checks: {},
variant: { type: 'string', enum: ['shortText'] },
},
required: ['label'],
},
Column: {
type: 'object',
properties: {
children: {}, // string[] | ChildListTemplate
},
},
Card: {
type: 'object',
properties: {
child: { type: 'string' },
children: {},
},
},
},
};
// Register BEFORE the React tree mounts
registerCatalog(myCatalog);Then reference in createSurface:
{
"version": "v0.9",
"createSurface": { "surfaceId": "my_surface", "catalogId": "local://my_catalog.json" }
}---
Remote Catalog Loading
For remote catalogs, provide a full URL as catalogId. The Box fetches and caches it automatically:
{
"version": "v0.9",
"createSurface": {
"surfaceId": "s1",
"catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
}
}⚠️catalogIdURIs are identifiers, not runtime-fetched resources in production. For custom catalogs, always useregisterCatalog()to avoid network dependency.
---
Basic Catalog (Pre-built)
The A2UI Basic Catalog includes general-purpose components. Use for prototyping:
| Component | Key Props |
|---|---|
Text | text (DynamicString, supports basic Markdown) |
Image | url, altText |
Icon | name |
Video | url |
AudioPlayer | url |
Row | children |
Column | children |
List | children (supports template mode) |
Card | child / children |
Tabs | array of { title, child } |
Divider | orientation |
Modal | dialog triggered by button |
Button | text, action, checks, variant |
CheckBox | label, value |
TextField | label, value, checks, variant |
DateTimeInput | label, value |
ChoicePicker | label, options, value, variant |
Slider | label, value, min, max |
---
Catalog Management APIs
import { registerCatalog, loadCatalog, validateComponent, clearCatalogCache } from '@ant-design/x-card';
// Register a local catalog
registerCatalog(catalog: Catalog): void
// Load a catalog by ID (checks local registry first, then fetches remote)
const catalog = await loadCatalog('local://my_catalog.json');
// Validate a component against a catalog
const isValid = validateComponent(catalog, 'Button', { text: 'Click me' });
// Clear the catalog cache (useful in tests)
clearCatalogCache();---
Naming & Versioning Rules
| Change Type | Version Impact |
|---|---|
| Add/remove container component (Grid, Accordion) | Breaking — major version bump |
| Add leaf component (Badge, Tooltip) | Non-breaking |
| Add optional property | Non-breaking |
| Remove property | Non-breaking |
| Add required property without default | Breaking |
| Change field type | Breaking |
CatalogId convention:
local://coffee_booking_catalog.json ← local registry
https://company.com/catalogs/v2/catalog.json ← remote, versioned URI---
Graceful Degradation
Renderers must handle unknown components gracefully:
- Unknown component type → render a placeholder text, not a crash
- Unknown prop on known component → silently ignored, component renders normally
- Removed component → no longer sent, client unaffected
A2UI Command Reference (v0.9)
This file covers all four server→client commands in XAgentCommand_v0_9. Read this when you need to understand command structure or build a command sequence.
---
Message Envelope
Every v0.9 command must include "version": "v0.9":
type XAgentCommand_v0_9 =
| { version: 'v0.9'; createSurface: CreateSurfacePayload }
| { version: 'v0.9'; updateComponents: UpdateComponentsPayload }
| { version: 'v0.9'; updateDataModel: UpdateDataModelPayload }
| { version: 'v0.9'; deleteSurface: DeleteSurfacePayload };---
createSurface
Initializes a new UI surface. Must be sent before any updateComponents or updateDataModel for that surfaceId.
interface CreateSurfacePayload {
surfaceId: string; // Unique surface identifier
catalogId: string; // Catalog URI or local:// identifier
theme?: {
primaryColor?: string; // Hex color e.g. "#00BFFF"
iconUrl?: string; // Agent logo URL
agentDisplayName?: string; // Display name in multi-agent systems
};
sendDataModel?: boolean; // If true, full data model sent with every action. Default: false
}{
"version": "v0.9",
"createSurface": {
"surfaceId": "booking_form",
"catalogId": "local://booking_catalog.json",
"theme": { "primaryColor": "#1677ff" }
}
}---
updateComponents
Sends a flat list of components (adjacency list). Can be called multiple times to add/replace components.
interface UpdateComponentsPayload {
surfaceId: string;
components: BaseComponent_v0_9[];
}
interface BaseComponent_v0_9 {
id: string; // Unique component ID within surface
component: string; // Component type name (must be in catalog)
child?: string; // Single child component ID
children?: string[] | ChildListTemplate; // Multiple child IDs or template
[key: string]: any | { path: string }; // All props support data binding
}
// Template mode for List components
interface ChildListTemplate {
path: string; // JSON Pointer to array in data model
componentId: string; // Template component ID to repeat per item
}Rules:
- One component must have
id: "root"— this is the tree root - References to child components that haven't arrived yet are allowed (streaming)
- Components are stored in a flat map; tree is reconstructed at render
{
"version": "v0.9",
"updateComponents": {
"surfaceId": "booking_form",
"components": [
{ "id": "root", "component": "Column", "children": ["title", "name_input", "submit_btn"] },
{ "id": "title", "component": "Text", "text": "Book a Table", "variant": "h1" },
{
"id": "name_input",
"component": "TextField",
"label": "Your Name",
"value": { "path": "/form/name" }
},
{
"id": "submit_btn",
"component": "Button",
"text": "Confirm",
"action": {
"event": {
"name": "confirm_booking",
"context": { "name": { "path": "/form/name" } }
}
}
}
]
}
}---
updateDataModel
Updates a value in the surface's data model at a JSON Pointer path. Uses upsert semantics.
interface UpdateDataModelPayload {
surfaceId: string;
path?: string; // JSON Pointer (RFC 6901). Defaults to "/" (full replace)
value?: any; // New value. If omitted, removes key at path
}Three patterns:
// Set a nested value
{ "version": "v0.9", "updateDataModel": { "surfaceId": "s1", "path": "/user/name", "value": "Alice" } }
// Replace entire model
{ "version": "v0.9", "updateDataModel": { "surfaceId": "s1", "value": { "user": { "name": "Alice" } } } }
// Remove a key (omit value)
{ "version": "v0.9", "updateDataModel": { "surfaceId": "s1", "path": "/user/tempData" } }Streaming pattern — send structure first, then
{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"local://cat.json"}}
{"version":"v0.9","updateComponents":{"surfaceId":"s1","components":[...]}}
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/list","value":[]}}
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/list/0","value":{"name":"Item A"}}}
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/list/1","value":{"name":"Item B"}}}---
deleteSurface
Removes a surface and all its components and data model.
interface DeleteSurfacePayload {
surfaceId: string;
}{ "version": "v0.9", "deleteSurface": { "surfaceId": "booking_form" } }---
v0.8 vs v0.9
| Aspect | v0.8 (deprecated) | v0.9 (recommended) |
|---|---|---|
| Version field | None | "version": "v0.9" required |
| Surface creation | Implicit on first updateComponents | Explicit createSurface |
| Component structure | Nested { "Button": { props } } | Flat { id, component: "Button", ...props } |
| Data updates | dataModelUpdate with contents array | updateDataModel with path + value |
| Data model init | contents: [{ key, valueString/valueMap }] | updateDataModel with JSON path |
| String literals | { "literalString": "text" } | Plain string "text" |
v0.8 example (do not use for new work):
{
"updateComponents": {
"surfaceId": "s1",
"catalogId": "...",
"components": [{ "id": "btn", "component": { "Button": { "text": "Click" } } }]
}
}v0.9 equivalent:
{
"version": "v0.9",
"updateComponents": {
"surfaceId": "s1",
"components": [{ "id": "btn", "component": "Button", "text": "Click" }]
}
}Data Binding Reference
This file covers how component props connect to the surface data model. Read this when binding component values to live data or building template lists.
---
Core Concept
Every surface has an independent JSON data model. Component props can read from it via { path: "/json/pointer" } syntax. When the data model updates, bound components re-render automatically.
---
JSON Pointer Syntax (RFC 6901)
| Path | Accesses |
|---|---|
/user/name | dataModel.user.name |
/cart/items/0 | dataModel.cart.items[0] |
/cart/items/0/price | dataModel.cart.items[0].price |
Absolute paths start with / — always resolve from model root. Relative paths (no leading /) — resolve relative to collection scope inside a template.
---
Literal vs Bound Values
// Literal — static, does not react to data changes
{ id: 'title', component: 'Text', text: 'Hello World' }
// Data-bound — reads from /user/name, re-renders on change
{ id: 'title', component: 'Text', text: { path: '/user/name' } }Any prop can be a literal or a { path } object. This includes strings, numbers, booleans, and string lists.
---
Dynamic Types
| Type | Literal form | Bound form |
|---|---|---|
DynamicString | "text" | { "path": "/data/str" } |
DynamicNumber | 42 | { "path": "/data/num" } |
DynamicBoolean | true | { "path": "/data/flag" } |
DynamicStringList | ["a","b"] | { "path": "/data/list" } |
---
Two-Way Binding (Input Components)
Input components bind their value prop for both read and write:
// TextField — displays /form/email, updates it on user input
{ id: 'email', component: 'TextField', label: 'Email', value: { path: '/form/email' } }
// CheckBox — toggles /form/subscribe
{ id: 'subscribe', component: 'CheckBox', label: 'Subscribe', value: { path: '/form/subscribe' } }
// ChoicePicker — updates /form/plan
{ id: 'plan', component: 'ChoicePicker', label: 'Plan', options: ['free','pro'], value: { path: '/form/plan' } }
// Slider — updates /form/quantity
{ id: 'qty', component: 'Slider', label: 'Quantity', min: 1, max: 10, value: { path: '/form/quantity' } }Read: component displays the current value from the data model. Write: user interaction immediately updates the data model at that path. Reactivity: all other components bound to the same path re-render.
⚠️ Server sync happens only on explicit action (button click), not on every keystroke.
---
Template Iteration (List Components)
Use children as a template object to iterate over an array in the data model:
{
"id": "product_list",
"component": "List",
"children": {
"path": "/products",
"componentId": "product_card_template"
}
}The product_card_template component is instantiated once per item in /products. Inside the template, paths are relative to each array item:
{ "id": "product_card_template", "component": "Card", "child": "product_name" }
{ "id": "product_name", "component": "Text", "text": { "path": "name" } }With data model { "products": [{ "name": "Apple" }, { "name": "Banana" }] }, { "path": "name" } resolves to /products/0/name and /products/1/name for each instance.
---
Reactive Updates
Send targeted updateDataModel commands to update only what changed — no need to resend component structure:
// Stream in list items one by one
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/restaurants/0","value":{"name":"Bella Italia","rating":4.5}}}
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/restaurants/1","value":{"name":"Tokyo Ramen","rating":4.8}}}
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/ui/loading","value":false}}---
Data Model Organization (Best Practice)
Organize state by domain:
{
"user": { "name": "Alice", "email": "alice@example.com" },
"form": { "name": "", "date": null, "guests": 2 },
"ui": { "loading": false, "step": 1 },
"results": []
}- Use
ui.*for UI state (loading, step, visibility) - Pre-compute display values on the agent side (e.g., send
"$19.99"rather than raw19.99) - Send granular updates targeting only changed paths — not full model replacements on each change
@ant-design/x-card
@ant-design/x-card is a dynamic card rendering component based on the A2UI protocol, enabling AI Agents to dynamically build and render interactive UIs through structured JSON message streams.
What is A2UI?
A2UI (Agent-to-User Interface) is an open protocol that allows AI Agents to describe interaction intent through declarative JSON message sequences, which the frontend runtime dynamically renders into native UI components.
Core Design Principles
A2UI is built on three core ideas:
1. Streaming Messages: UI updates flow from the Agent to the client as a sequence of JSON messages 2. Declarative Components: UI is described as data, not code 3. Data Binding: UI structure is decoupled from application state, enabling reactive updates
Why A2UI?
Unlike traditional approaches where AI generates HTML directly, A2UI uses structured data streams with significant advantages:
| Feature | A2UI | AI-generated HTML |
|---|---|---|
| Security | Only uses predefined component catalog, no code execution risk | May contain malicious scripts, injection risk |
| Cross-platform | One data structure auto-adapts to Web, mobile, and other native components | HTML requires extra adaptation per platform |
| Streaming Rendering | Supports progressive rendering for smooth UX | Requires complete response before rendering |
| LLM-friendly | Flat JSON structure supports incremental generation, reduces AI burden | Requires generating full HTML structure, prone to syntax errors |
| Maintenance | Components managed centrally, updates only require client library changes | Each HTML interface needs individual debugging |
Data Flow Architecture
A2UI follows the unidirectional data flow principle, ensuring predictable data direction:
Agent (LLM) → A2UI Generator → Transport (SSE/WebSocket/A2A)
↓
Client (Stream Reader) → Message Parser → Renderer → Native UIData Flow Lifecycle
Using a restaurant booking as an example:
sequenceDiagram
participant User
participant Client
participant Agent
User->>Client: "Book a table for 2 tomorrow at 7pm"
Client->>Agent: Send user request
Note over Agent: 1. Create UI container
Agent->>Client: createSurface { surfaceId: "booking" }
Note over Agent: 2. Define UI structure
Agent->>Client: updateComponents { components: [...] }
Note over Agent: 3. Populate initial data
Agent->>Client: updateDataModel { datetime: "2025-12-16T19:00", guests: 2 }
Note over Client: 4. Render booking form
Client->>User: Show date picker and guest count input
User->>Client: Change guest count to 3
Note over Client: Auto-updates /reservation/guests
User->>Client: Click "Confirm" button
Client->>Agent: userAction { name: "confirm", context: { guests: 3 } }
Note over Agent: 5. Handle user action
Agent->>Client: deleteSurface { surfaceId: "booking" }
Agent->>Client: Create success confirmation UIProtocol Versions
@ant-design/x-card supports both v0.8 and v0.9 of the A2UI protocol. Understanding the differences helps you choose the right version and migrate if needed.
Version Comparison
| Feature | v0.8 | v0.9 |
|---|---|---|
| Version field | No explicit version field | Explicit version: 'v0.9' field |
| Surface creation | Implicit (auto-created on first updateComponents) | Explicit createSurface command |
| Data model update | Uses contents array | Uses path and value fields |
| Component definition | More complex nested structure | Simpler flat structure |
| Recommendation | Deprecated, compatibility only | Recommended |
v0.8 Message Format (Deprecated)
v0.8 uses implicit Surface creation — a Surface is automatically created when the Agent sends the first updateComponents:
// v0.8 has no explicit version field
{
updateComponents: {
surfaceId: 'booking',
catalogId: 'https://example.com/catalogs/booking/v1/catalog.json',
components: [
{
id: 'root',
component: 'Column',
children: ['header', 'content']
}
]
}
}Data model updates use the contents array:
{
updateDataModel: {
surfaceId: 'booking',
contents: [
{
op: 'replace',
path: '/reservation/guests',
value: 3
}
]
}
}v0.9 Message Format (Recommended)
v0.9 introduces explicit version identification and a Surface creation command, making the protocol clearer and more controllable:
// Explicitly create Surface
{
version: 'v0.9',
createSurface: {
surfaceId: 'booking',
catalogId: 'https://example.com/catalogs/booking/v1/catalog.json'
}
}
// Update components
{
version: 'v0.9',
updateComponents: {
surfaceId: 'booking',
components: [
{
id: 'root',
component: 'Column',
children: ['header', 'content']
}
]
}
}Data model updates use the more intuitive path and value fields:
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'booking',
path: '/reservation/guests',
value: 3
}
}Migration Guide
If you are using v0.8, follow these steps to migrate to v0.9:
1. Add Version Field
Add version: 'v0.9' to all messages:
// v0.8
{ updateComponents: { ... } }
// v0.9
{ version: 'v0.9', updateComponents: { ... } }2. Explicitly Create Surface
Send createSurface before updateComponents:
// v0.8: implicit creation
{ updateComponents: { surfaceId: 'booking', catalogId: '...', components: [...] } }
// v0.9: explicit creation
[
{ version: 'v0.9', createSurface: { surfaceId: 'booking', catalogId: '...' } },
{ version: 'v0.9', updateComponents: { surfaceId: 'booking', components: [...] } }
]3. Simplify Data Model Updates
Replace the contents array with path + value:
// v0.8
{
updateDataModel: {
surfaceId: 'booking',
contents: [
{ op: 'replace', path: '/guests', value: 3 }
]
}
}
// v0.9
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'booking',
path: '/guests',
value: 3
}
}4. Batch Data Updates
v0.9 supports updating entire objects, reducing message count:
// v0.8: multiple messages required
[
{ updateDataModel: { surfaceId: 'booking', contents: [{ op: 'add', path: '/date', value: '2025-12-16' }] } },
{ updateDataModel: { surfaceId: 'booking', contents: [{ op: 'add', path: '/guests', value: 2 }] } }
]
// v0.9: one message is enough
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'booking',
path: '/reservation',
value: { date: '2025-12-16', guests: 2 }
}
}Backward Compatibility
@ant-design/x-card supports both versions simultaneously:
import type { XAgentCommand_v0_8, XAgentCommand_v0_9 } from '@ant-design/x-card';
// Auto-detect version and handle correctly
const commands: (XAgentCommand_v0_8 | XAgentCommand_v0_9)[] = [
// v0.8 message
{
updateComponents: {
/* ... */
},
},
// v0.9 message
{
version: 'v0.9',
createSurface: {
/* ... */
},
},
];
<XCard.Box commands={commands}>{/* ... */}</XCard.Box>;The component automatically detects the protocol version based on the presence of the version field and handles messages correctly.
Core Message Types
@ant-design/x-card fully implements the A2UI v0.9 core command system:
1. createSurface — Create UI Container
Creates a new UI container (Surface). Each Surface has its own independent component tree and data model.
{
version: 'v0.9',
createSurface: {
surfaceId: 'booking', // Unique surface identifier
catalogId: 'https://example.com/catalogs/booking/v1/catalog.json' // Component catalog
}
}2. updateComponents — Update Component Structure
Defines or updates UI components in a Surface using the adjacency list model.
{
version: 'v0.9',
updateComponents: {
surfaceId: 'booking',
components: [
{ id: 'root', component: 'Column', children: ['header', 'guests-field', 'submit-btn'] },
{ id: 'header', component: 'Text', text: 'Confirm Reservation', variant: 'h1' },
{ id: 'guests-field', component: 'TextField', label: 'Guests', value: { path: '/reservation/guests' } },
{
id: 'submit-btn',
component: 'Button',
variant: 'primary',
child: 'submit-text',
action: {
event: { name: 'confirm', context: { details: { path: '/reservation' } } }
}
}
]
}
}3. updateDataModel — Update Data Model
Updates the Surface's application state, triggering reactive UI updates.
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'booking',
path: '/reservation',
value: {
datetime: '2025-12-16T19:00:00Z',
guests: 2
}
}
}4. deleteSurface — Delete Surface
Removes the specified Surface and all its components and data model.
{
version: 'v0.9',
deleteSurface: {
surfaceId: 'booking'
}
}Data Binding System
A2UI separates UI structure from application state, enabling reactive updates through data binding.
Data Model
Each Surface has an independent JSON data model:
{
"user": { "name": "Alice", "email": "alice@example.com" },
"reservation": { "datetime": "2025-12-16T19:00:00Z", "guests": 2 }
}JSON Pointer Paths
Uses RFC 6901 standard JSON Pointer to access
/user/name→"Alice"/reservation/guests→2
Literal vs. Path Binding
Component properties can use literal values or data binding:
// Literal (static)
{ id: 'title', component: 'Text', text: 'Welcome' }
// Path binding (dynamic)
{ id: 'username', component: 'Text', text: { path: '/user/name' } }When /user/name changes from "Alice" to "Bob", the text updates automatically.
Two-way Binding
Interactive components can automatically update the data model:
{ id: 'name-input', component: 'TextField', value: { path: '/form/name' } }User input automatically updates /form/name.
Action Event Handling
User interactions are passed back to the Agent via action events.
// Component definition
{
id: 'submit-btn',
component: 'Button',
action: {
event: {
name: 'confirm_booking',
context: {
date: { path: '/reservation/datetime' },
guests: { path: '/reservation/guests' }
}
}
}
}When the user clicks the button, the client sends:
{
version: 'v0.9',
action: {
name: 'confirm_booking',
surfaceId: 'booking',
sourceComponentId: 'submit-btn',
timestamp: '2025-12-16T19:05:00Z',
context: { date: '2025-12-16T19:00:00Z', guests: 3 }
}
}Component Catalog
The Catalog defines available components and their property schemas, ensuring type safety and validation.
registerCatalog(catalog);
<XCard.Box
components={{
Text: MyTextComponent,
Button: MyButtonComponent,
TextField: MyTextFieldComponent,
}}
>
{/* ... */}
</XCard.Box>;Core Features
1. Progressive Rendering
Users see the UI build up incrementally without waiting for the full response.
2. Adjacency List Model
Uses a flat component list instead of a nested tree structure — LLM-friendly, supports incremental updates, and fault-tolerant.
3. Component Validation
Automatically validates component properties against the Catalog. Friendly errors in development, graceful degradation in production.
4. Type Safety
Full TypeScript type definitions:
import type {
XAgentCommand_v0_9,
XAgentCommand_v0_8,
ActionPayload,
Catalog,
CatalogComponent,
} from '@ant-design/x-card';Installation
npm install @ant-design/x-card
# or
yarn add @ant-design/x-card
# or
pnpm add @ant-design/x-cardQuick Start
import { XCard, registerCatalog } from '@ant-design/x-card';
import type { XAgentCommand_v0_9, Catalog, ActionPayload } from '@ant-design/x-card';
const catalog: Catalog = {
catalogId: 'my-app-catalog',
components: {
Text: {
/* ... */
},
Button: {
/* ... */
},
},
};
registerCatalog(catalog);
const commands: XAgentCommand_v0_9[] = [
{ version: 'v0.9', createSurface: { surfaceId: 'booking', catalogId: 'my-app-catalog' } },
{
version: 'v0.9',
updateComponents: {
surfaceId: 'booking',
components: [
/* ... */
],
},
},
];
function App() {
const handleAction = (payload: ActionPayload) => {
console.log('Action triggered:', payload.name, payload.context);
};
return (
<XCard.Box
commands={commands}
onAction={handleAction}
components={{ Text: MyTextComponent, Button: MyButtonComponent }}
>
<XCard.Card id="booking" />
</XCard.Box>
);
}Use Cases
- AI Assistant UIs: Let AI Agents dynamically generate forms, cards, and interactive interfaces
- Smart Forms: Dynamically adjust form structure and validation rules based on user input
- Data Visualization: Dynamically generate charts, lists, and data display components
- Workflow Orchestration: Render different stage UIs based on business processes
- Multi-turn Conversations: Embed dynamic interactive components in chat interfaces
- Personalized UIs: Customize UI based on user preferences and usage scenarios
Next Steps
- See A2UI v0.9 for the latest protocol spec and examples
- Read the A2UI Official Docs for protocol design philosophy
- Browse Component Structure to learn the adjacency list model
- Reference Data Binding to master reactive updates
Usage Guide
This file covers setup, multi-surface patterns, and streaming progressive rendering. Read this for end-to-end integration examples.
---
Basic Setup
Minimum working setup:
import React, { useState } from 'react';
import { XCard, registerCatalog } from '@ant-design/x-card';
import type { XAgentCommand_v0_9, ActionPayload, Catalog } from '@ant-design/x-card';
// 1. Define catalog
const catalog: Catalog = {
catalogId: 'local://basic.json',
components: {
Text: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
Column: { type: 'object', properties: { children: {} } },
Button: {
type: 'object',
properties: { text: { type: 'string' }, action: {} },
required: ['text'],
},
},
};
registerCatalog(catalog);
// 2. Custom component implementations
const components = {
Text: ({ text }: { text: string }) => <p>{text}</p>,
Column: ({ children }: { children: React.ReactNode }) => (
<div style={{ display: 'flex', flexDirection: 'column' }}>{children}</div>
),
Button: ({ text, onAction, action }: any) => (
<button onClick={() => onAction?.(action?.event?.context ?? {})}>{text}</button>
),
};
// 3. Build initial commands
const initialCommands: XAgentCommand_v0_9[] = [
{ version: 'v0.9', createSurface: { surfaceId: 'main', catalogId: 'local://basic.json' } },
{
version: 'v0.9',
updateComponents: {
surfaceId: 'main',
components: [
{ id: 'root', component: 'Column', children: ['greeting', 'btn'] },
{ id: 'greeting', component: 'Text', text: { path: '/name' } },
{
id: 'btn',
component: 'Button',
text: 'Say Hi',
action: { event: { name: 'greet', context: {} } },
},
],
},
},
{ version: 'v0.9', updateDataModel: { surfaceId: 'main', path: '/name', value: 'Alice' } },
];
// 4. Component
export default function App() {
const [cmds, setCmds] = useState(initialCommands);
const handleAction = (payload: ActionPayload) => {
if (payload.name === 'greet') {
setCmds((prev) => [
...prev,
{ version: 'v0.9', updateDataModel: { surfaceId: 'main', path: '/name', value: 'Bob' } },
]);
}
};
return (
<XCard.Box commands={cmds} components={components} onAction={handleAction}>
<XCard.Card id="main" />
</XCard.Box>
);
}---
Multi-Surface Setup
Multiple independent surfaces in one Box:
export default function MultiSurface() {
const [cmds, setCmds] = useState<XAgentCommand_v0_9[]>([
// Surface 1
{ version: 'v0.9', createSurface: { surfaceId: 'profile', catalogId: 'local://cat.json' } },
{
version: 'v0.9',
updateComponents: {
surfaceId: 'profile',
components: [{ id: 'root', component: 'Text', text: { path: '/user/name' } }],
},
},
{
version: 'v0.9',
updateDataModel: { surfaceId: 'profile', path: '/user/name', value: 'Alice' },
},
// Surface 2
{ version: 'v0.9', createSurface: { surfaceId: 'cart', catalogId: 'local://cat.json' } },
{
version: 'v0.9',
updateComponents: {
surfaceId: 'cart',
components: [{ id: 'root', component: 'Text', text: { path: '/total' } }],
},
},
{ version: 'v0.9', updateDataModel: { surfaceId: 'cart', path: '/total', value: '$42.00' } },
]);
return (
<XCard.Box commands={cmds} components={components}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<XCard.Card id="profile" />
<XCard.Card id="cart" />
</div>
</XCard.Box>
);
}---
Streaming
Append commands incrementally as the agent streams responses:
export default function StreamingDemo() {
const [cmds, setCmds] = useState<XAgentCommand_v0_9[]>([]);
const startStream = async () => {
// Step 1: Create surface immediately
setCmds([
{ version: 'v0.9', createSurface: { surfaceId: 'results', catalogId: 'local://cat.json' } },
{
version: 'v0.9',
updateComponents: {
surfaceId: 'results',
components: [
{ id: 'root', component: 'Column', children: ['loading', 'list'] },
{ id: 'loading', component: 'Text', text: { path: '/ui/status' } },
{
id: 'list',
component: 'List',
children: { path: '/items', componentId: 'item_tmpl' },
},
{ id: 'item_tmpl', component: 'Text', text: { path: 'name' } },
],
},
},
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'results',
value: { ui: { status: 'Loading...' }, items: [] },
},
},
]);
// Step 2: Stream data in progressively
for (let i = 0; i < 5; i++) {
await delay(500);
setCmds((prev) => [
...prev,
{
version: 'v0.9',
updateDataModel: {
surfaceId: 'results',
path: `/items/${i}`,
value: { name: `Item ${i + 1}` },
},
},
]);
}
// Step 3: Done
setCmds((prev) => [
...prev,
{
version: 'v0.9',
updateDataModel: { surfaceId: 'results', path: '/ui/status', value: 'Complete!' },
},
]);
};
return (
<div>
<button onClick={startStream}>Start</button>
<XCard.Box commands={cmds} components={components}>
<XCard.Card id="results" />
</XCard.Box>
</div>
);
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));---
Stable components Prop
Keep the components map stable to prevent unnecessary re-renders:
// ✅ Module-level constant — always stable
const COMPONENTS = { Text, Button, Column, Card };
// ✅ useMemo when deps are static
const components = useMemo(() => ({ Text, Button }), []);
// ❌ Inline object — recreated every render, causes Card to remount
<XCard.Box components={{ Text, Button }} ...>---
Teardown
When done, send deleteSurface to clean up:
const teardown = () => {
setCmds((prev) => [...prev, { version: 'v0.9', deleteSurface: { surfaceId: 'main' } }]);
};