Component Registry
API reference for registerComponent, ComponentRenderer, and the built-in components
Render data-driven component specs — a component name plus props — with Zod-validated safety. @urun-sh/react ships a small client-side registry plus five built-in components. Your app decides where specs come from: a named data stream (read with useStreamMessages), a doc field, or any app-defined envelope. The registry only resolves and validates — it never renders anything you didn't register.
Public API at a glance
| Name | Description |
|---|---|
registerComponent(name, component, schema) | Register a React component for spec-driven rendering |
ComponentRenderer | Resolve a spec by name, validate props, render (or fall back) |
ProgressCard | Step progress with optional label |
StatusBadge | Agent state indicator badge |
TextStream | Streaming text output with cursor |
ImageFrame | Rendered image with optional caption |
MetricsPanel | Key-value metrics grid |
registerComponent()
function registerComponent(
name: string,
component: React.ComponentType<any>,
schema: ZodSchema
): voidRegister a React component under a name. Once registered, any spec carrying that name renders through ComponentRenderer with Zod-validated props.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string | required | Component name (must match the spec's component field) |
component | React.ComponentType | required | React component to render |
schema | ZodSchema | required | Zod schema for prop validation |
Register components before rendering specs (module scope is the usual place). Invalid props render a fallback error state, not a crash.
import { registerComponent } from '@urun-sh/react'
import { z } from 'zod'
const MyCardProps = z.object({ title: z.string(), body: z.string() })
function MyCard({ title, body }: z.infer<typeof MyCardProps>) {
return (
<div className="card">
<h3>{title}</h3>
<p>{body}</p>
</div>
)
}
registerComponent('MyCard', MyCard, MyCardProps)ComponentRenderer
<ComponentRenderer name={spec.component} props={spec.props} fallback={<Skeleton />} />Resolves name in the registry, validates props with the registered Zod schema, and renders the component. Unknown names or invalid props render fallback (or a small inline error state) and log a console warning — never a crash.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Registered component name (the spec's component field) |
props | unknown | required | The spec's props, validated against the registered schema |
fallback | ReactNode | — | Rendered when resolution/validation fails |
A typical spec source is a named data stream the runtime emits on — the spec shape (component + props) is your app's convention, not a platform protocol:
import { ComponentRenderer, useStreamMessages } from '@urun-sh/react'
function StatusPanel({ session }) {
const specs = useStreamMessages(session, 'ui') // ctx.stream("ui").emit({component, props}) on Python
return specs.map(({ at, payload }) => {
const spec = payload as { component: string; props: unknown }
return <ComponentRenderer key={at} name={spec.component} props={spec.props} />
})
}# Python runtime — emit specs on a named data stream
ctx.stream("ui").emit({
"component": "ProgressCard",
"props": {"step": 42, "total": 100, "label": "Generating frames"},
})Import the package CSS once (import '@urun-sh/react/styles.css') when using the styled built-ins.
Built-in components
Five components ship with @urun-sh/react, each with a styled variant, a headless hook, and a Zod schema export (ProgressCardSchema, …). Register the ones you drive from spec data — registerComponent('ProgressCard', ProgressCard, ProgressCardSchema) — or use them directly as plain React components.
| Component | Hook | Purpose |
|---|---|---|
ProgressCard | useProgressCard | Step progress with an optional label |
StatusBadge | useStatusBadge | Compact status badge |
TextStream | useTextStream | Token-by-token streaming text |
ImageFrame | useImageFrame | A rendered image with optional caption |
MetricsPanel | useMetricsPanel | Key/value metrics grid |
ProgressCard
const ProgressCardSchema = z.object({
step: z.number().min(0),
total: z.number().min(1),
label: z.string().optional(),
variant: z.enum(['default', 'success', 'error']).default('default'),
})| Prop | Type | Default | Description |
|---|---|---|---|
step | number | required | Current step count |
total | number | required | Total step count |
label | string | — | Optional label above the progress bar |
variant | 'default' | 'success' | 'error' | 'default' | Color variant |
StatusBadge
const StatusBadgeSchema = z.object({
state: z.enum(['thinking', 'generating', 'idle', 'error']),
message: z.string().optional(),
})| Prop | Type | Default | Description |
|---|---|---|---|
state | 'thinking' | 'generating' | 'idle' | 'error' | required | Current state |
message | string | — | Optional override message |
TextStream
const TextStreamSchema = z.object({
text: z.string(),
streaming: z.boolean().default(false),
})| Prop | Type | Default | Description |
|---|---|---|---|
text | string | required | Current accumulated text |
streaming | boolean | false | Whether to show the streaming cursor |
ImageFrame
const ImageFrameSchema = z.object({
src: z.string().url(),
alt: z.string().optional(),
caption: z.string().optional(),
})| Prop | Type | Default | Description |
|---|---|---|---|
src | string | required | Image URL |
alt | string | — | Alt text for accessibility |
caption | string | — | Caption displayed below the image |
MetricsPanel
const MetricsPanelSchema = z.object({
metrics: z.array(z.object({
label: z.string(),
value: z.union([z.string(), z.number()]),
unit: z.string().optional(),
})),
})| Prop | Type | Default | Description |
|---|---|---|---|
metrics | Array<{ label, value, unit? }> | required | Metric rows (unit is an optional suffix, e.g. "fps", "ms") |
Validation is safe by default
Registered components are validated at render time. If a spec's props fail the Zod schema, the SDK renders a fallback error state instead of throwing — spec producers can push arbitrary data and the client stays resilient.