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 response text.
libraryLibraryLibrary created by createLibrary(...).
isStreamingbooleanIndicates stream is in progress.
onAction(event: ActionEvent) => voidReceives structured action events from interactive components.
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 shown while queries are fetching. Defaults to a spinner.
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 — tools are just async functions
<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
excess-argsparserMore positional args than schema params (extras dropped, still renders)
inline-reservedparserQuery/Mutation used inline instead of top-level
parse-failedparserResponse produced no renderable root
parse-exceptionparserParser crashed on malformed input
tool-not-foundquery/mutationTool name not found in toolProvider
runtime-errorruntimeExpression evaluation failed on a prop
render-errorruntimeReact component threw during render

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 — useful for real-time debugging.
  • 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()
  • 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