OpenUI

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

PropTypeDescription
responsestring | nullRaw OpenUI Lang text, or a stored message. The Renderer renders its content and ignores the context. See Message Protocol.
libraryLibraryLibrary created by createLibrary(...).
isStreamingbooleanIndicates stream is in progress.
onAction(event: LibraryActionEvent<L>) => voidReceives structured action events from interactive components. Typed from the library's custom actions.
onStateUpdate(state: Record<string, any>) => voidCalled on form field changes with the raw field state map.
initialStateRecord<string, any>Hydrates form state on load (e.g. from persisted message).
onParseResult(result: ParseResult | null) => voidDebug/inspect latest parse result.
toolProviderRecord<string, (args: Record<string, unknown>) => Promise<unknown>> | McpClientLike | nullHandles Query() / Mutation() tool calls. Pass a function map or an MCP client.
queryLoaderReact.ReactNodeCustom loading indicator; defaults to a spinner.
childrenReact.ReactNodeOptional Renderer slot composition; omit to keep content visible with a loading indicator.
onError(errors: OpenUIError[]) => voidStructured, 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:

CodeSourceDescription
unknown-componentparserComponent name not in the library
missing-requiredparserRequired prop not provided
null-requiredparserRequired prop explicitly null
type-mismatchparserArgument does not match the prop's type or enum
excess-argsparserMore positional args than schema params (extras dropped, still renders)
inline-reservedparserQuery/Mutation used inline instead of top-level
unknown-functionparser@Name(...) is not a built-in function, action, or library function
no-rootparserNo usable entry statement (see Entry Statement)
parse-failedparserResponse text produced no statements
parse-exceptionparserParser crashed on malformed input
tool-not-foundquery/mutationTool name not found in toolProvider
tool-errorquery/mutationThe tool call failed
mcp-errorquery/mutationThe MCP tool returned an error
runtime-errorruntimeExpression evaluation or a library function call failed on a prop
render-errorruntimeReact 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.orphaned lists defined but unreachable statements on every chunk. Use it to debug while streaming.
  • There is no nodePlaceholder prop 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 $variable binding (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>,
});

On this page