Interactivity
Handle actions, forms, and state in OpenUI components.
OpenUI components can be interactive. The Renderer manages form state automatically and exposes callbacks for actions and persistence.
Actions
When a user clicks a button or follow-up, the component calls triggerAction. The Renderer wraps this into an ActionEvent and fires onAction.
<Renderer
library={myLibrary}
response={content}
onAction={(event) => {
if (event.type === "continue_conversation") {
// event.humanFriendlyMessage — button label or follow-up text
// event.formState — field values at time of click
// event.formName — scoping form name, if any
}
}}
/>ActionEvent
| Field | Type | Description |
|---|---|---|
type | string | Action type (see built-in types below). |
params | Record<string, any> | Extra parameters from the component. |
humanFriendlyMessage | string | Display label for the action. |
formState | Record<string, any> | undefined | Raw field state at time of action. |
formName | string | undefined | Form that scoped the action, if any. |
Built-in action types
// Dispatched via onAction callback
enum BuiltinActionType {
ContinueConversation = "continue_conversation",
OpenUrl = "open_url",
}ContinueConversation: sends the user's intent back to the LLM (@ToAssistant).OpenUrl: opens a URL in a new tab (@OpenUrl).
The following action steps are handled internally by the runtime (not dispatched to onAction):
@Run(ref): executes a Mutation or re-fetches a Query@Set($var, value): changes a reactive$variable@Reset($var1, $var2): restores$variablesto their declared defaults
In openui-lang, actions are composed with Action([...]):
submitBtn = Button("Create", Action([@Run(createResult), @Run(tickets), @Reset($title)]))See Reactive State for $variables and Queries & Mutations for @Run.
Inline mode
When inlineMode is enabled in the prompt config, the LLM can respond with either:
- Code (fenced in triple backticks), for creating or changing the UI
- Text only, for answering questions without modifying the UI
The parser extracts code from fences automatically. Text outside fences is shown as chat.
Using triggerAction in components
Inside defineComponent, use the useTriggerAction hook:
const MyButton = defineComponent({
name: "MyButton",
description: "A clickable button.",
props: z.object({ label: z.string() }),
component: ({ props }) => {
const triggerAction = useTriggerAction();
return <button onClick={() => triggerAction(props.label)}>{props.label}</button>;
},
});triggerAction(userMessage, formName?, action?) accepts optional second and third arguments.
Reactive state ($variables)
In openui-lang, $variables create reactive state that components can read and write. When the LLM generates $days = "7" and passes it to a Select, the runtime creates a two-way binding automatically.
For component authors building custom libraries, the useStateField hook provides this binding:
import { useStateField, reactive } from "@openuidev/react-lang";
const MySelect = defineComponent({
name: "MySelect",
props: z.object({
name: z.string(),
value: reactive(z.string().optional()), // reactive() marks this prop as accepting $variable binding
items: z.array(SelectItem.ref),
}),
component: ({ props }) => {
const field = useStateField(props.name, props.value);
// field.value — current value (reads from $variable or form state)
// field.setValue(val) — updates the $variable and triggers re-evaluation
return (
<select value={field.value ?? ""} onChange={(e) => field.setValue(e.target.value)}>
{/* ... */}
</select>
);
},
});useStateField unifies form state and reactive $variable binding. When the prop is a $variable, setValue updates the store and triggers all dependent queries and expressions to re-evaluate.
See Reactive State for the language-level documentation.
Form state
The Renderer tracks field values automatically. Components use useStateField (preferred) or the lower-level useSetFieldValue and useGetFieldValue to read and write state.
Persistence
Use onStateUpdate to persist field state (e.g. to a message in your thread store) and initialState to hydrate it on load.
<Renderer
library={myLibrary}
response={content}
onStateUpdate={(state) => {
// state is a raw Record<string, any> of all field values
saveToBackend(state);
}}
initialState={loadedState}
/>onStateUpdate fires on every field change. The state format is opaque, so persist and hydrate it as-is.
Field hooks
Use these inside defineComponent renderers:
| Hook | Signature | Description |
|---|---|---|
useStateField | (name: string, value?: unknown) => { value: unknown, setValue: (v: unknown) => void } | Preferred. Unified form state + reactive $variable binding. |
useGetFieldValue | (formName: string | undefined, name: string) => any | Read a field's current value. |
useSetFieldValue | (formName: string | undefined, componentType: string | undefined, name: string, value: any, shouldTriggerSaveCallback?: boolean) => void | Write a field value. |
useFormName | () => string | undefined | Get the enclosing form's name. |
useSetDefaultValue | (options: { formName?, componentType, name, existingValue, defaultValue, shouldTriggerSaveCallback? }) => void | Set a default if no value exists. |
Validation
Form fields can declare validation rules. The Form component provides a validation context via useFormValidation.
interface FormValidationContextValue {
errors: Record<string, string | undefined>;
validateField: (name: string, value: unknown, rules: ParsedRule[]) => boolean;
registerField: (name: string, rules: ParsedRule[], getValue: () => unknown) => void;
unregisterField: (name: string) => void;
validateForm: () => boolean;
clearFieldError: (name: string) => void;
}Built-in validators include required, minLength, maxLength, min, max, pattern, and email. Custom validators can be added via builtInValidators.