The Renderer
Parse and render OpenUI Lang streams in React.
<Renderer /> converts OpenUI Lang text into React UI using your library.
Basic usage
import { Renderer } from "@openuidev/react-lang";
import { openuiLibrary } from "@openuidev/react-ui";
export function AssistantMessage({
content,
isStreaming,
}: {
content: string | null;
isStreaming: boolean;
}) {
return <Renderer library={openuiLibrary} response={content} isStreaming={isStreaming} />;
}Props
| Prop | Type | Description |
|---|---|---|
response | string | null | Raw OpenUI Lang text, or a stored message. The Renderer renders its content and ignores the context. See Message Protocol. |
library | Library | Library created by createLibrary(...). |
isStreaming | boolean | Indicates stream is in progress. |
onAction | (event: LibraryActionEvent<L>) => void | Receives structured action events from interactive components. Typed from the library's custom actions. |
onStateUpdate | (state: Record<string, any>) => void | Called on form field changes with the raw field state map. |
initialState | Record<string, any> | Hydrates form state on load (e.g. from persisted message). |
onParseResult | (result: ParseResult | null) => void | Debug/inspect latest parse result. |
toolProvider | Record<string, (args: Record<string, unknown>) => Promise<unknown>> | McpClientLike | null | Handles Query() / Mutation() tool calls. Pass a function map or an MCP client. |
queryLoader | React.ReactNode | Custom loading indicator; defaults to a spinner. |
children | React.ReactNode | Optional Renderer slot composition; omit to keep content visible with a loading indicator. |
onError | (errors: OpenUIError[]) => void | Structured, LLM-friendly errors from the parser and query system. Suitable for automated correction loops. Called with [] when resolved. |
Connecting tools
For UIs that use Query() and Mutation(), pass a toolProvider. Two options:
// Function map: one async function per tool
<Renderer
toolProvider={{
list_tickets: async (args) => fetch("/api/tickets").then((r) => r.json()),
create_ticket: async (args) =>
fetch("/api/tickets", { method: "POST", body: JSON.stringify(args) }).then((r) => r.json()),
}}
library={library}
response={code}
/>;
// MCP client: pass a configured MCP client directly
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL("/api/mcp")));
<Renderer toolProvider={client} library={library} response={code} />;These examples are simplified for demonstration. In production, you'll need to handle authentication, error boundaries, and connection lifecycle.
Error handling
onError receives structured errors suitable for sending back to the LLM for correction:
<Renderer
library={library}
response={code}
toolProvider={mcp}
onError={(errors) => {
if (!errors.length) return;
const msg = errors
.map(
(e) =>
`[${e.source}] ${e.statementId ? `"${e.statementId}": ` : ""}${e.message}${e.hint ? `\nHint: ${e.hint}` : ""}`,
)
.join("\n\n");
// Send back to LLM for self-correction
sendToLLM(`Fix these errors:\n\n${msg}`);
}}
/>Error codes:
| Code | Source | Description |
|---|---|---|
unknown-component | parser | Component name not in the library |
missing-required | parser | Required prop not provided |
null-required | parser | Required prop explicitly null |
type-mismatch | parser | Argument does not match the prop's type or enum |
excess-args | parser | More positional args than schema params (extras dropped, still renders) |
inline-reserved | parser | Query/Mutation used inline instead of top-level |
unknown-function | parser | @Name(...) is not a built-in function, action, or library function |
no-root | parser | No usable entry statement (see Entry Statement) |
parse-failed | parser | Response text produced no statements |
parse-exception | parser | Parser crashed on malformed input |
tool-not-found | query/mutation | Tool name not found in toolProvider |
tool-error | query/mutation | The tool call failed |
mcp-error | query/mutation | The MCP tool returned an error |
runtime-error | runtime | Expression evaluation or a library function call failed on a prop |
render-error | runtime | React component threw during render |
An error with severity: "warning" did not stop the page from rendering, for example a no-root fallback entry. Skip warnings when you send errors back to the LLM.
If no onError callback is provided, errors are logged to console.warn instead of being silently swallowed.
Streaming behavior
- Parser re-runs as chunks arrive.
- Forward references resolve when their statements arrive.
- Unresolved references and invalid components are dropped from arrays (not left as null holes).
meta.orphanedlists defined but unreachable statements on every chunk. Use it to debug while streaming.- There is no
nodePlaceholderprop in the current renderer API.
Actions
<Renderer
library={openuiLibrary}
response={content}
onAction={(event) => {
if (event.type === "continue_conversation") {
// event.humanFriendlyMessage: display label
// event.formState: raw field values at time of action
}
}}
/>Hooks for component authors
Use these inside components defined with defineComponent(...). See Reactive State for how $variables work in the language.
useStateField(name, value?)- reactive$variablebinding (preferred for inputs)useIsStreaming()useIsQueryLoading()useTriggerAction()useRenderNode()useFormValidation()useFormName()/useGetFieldValue()/useSetFieldValue()
In component renderers, renderNode is also passed directly as a prop.
Example with nested children
const Dashboard = defineComponent({
name: "Dashboard",
description: "Container",
props: z.object({ cards: z.array(StatCard.ref) }),
component: ({ props, renderNode }) => <div>{renderNode(props.cards)}</div>,
});