# State and forms

> Update reactive values, validate generated forms, inspect action payloads, and save and restore renderer state.

Source: https://ai.honestui.com/docs/generative/state

Generated interfaces can stay interactive after generation finishes. The runtime owns their reactive values and form fields; your application owns persistence and any action that changes external data.

## Save and restore a value [#save-and-restore-a-value]

Add to the count, save a snapshot, change the count again, then restore the snapshot. The example remounts the renderer with the saved state to demonstrate reopening a response.

```tsx
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Renderer, honestLibrary } from "@/registry/generative";

export const stateProgram = `$quantity = 1
root = Stack([TextContent("Example packing count: " + $quantity), buttons])
buttons = Buttons([Button("Add one", @Set($quantity, $quantity + 1), "primary"), Button("Reset count", @Reset($quantity), "secondary")])`;

export function GenerativeStateDemo() {
  const [state, setState] = React.useState<Record<string, unknown>>({});
  const [saved, setSaved] = React.useState<Record<string, unknown> | null>(null);
  const [initialState, setInitialState] = React.useState<Record<string, unknown>>({});
  const [version, setVersion] = React.useState(0);
  const [status, setStatus] = React.useState("Nothing saved yet.");
  return (
    <div className="grid min-w-0 gap-4 p-4" data-slot="generative-state-demo">
      <p className="m-0 text-sm text-muted-foreground">Local example. Add to the count, save it, change it, then restore.</p>
      <Renderer key={version} response={stateProgram} library={honestLibrary} initialState={initialState} onStateUpdate={setState} />
      <div className="flex flex-wrap gap-2">
        <Button variant="secondary" onClick={() => { setSaved(structuredClone(state)); setStatus("Snapshot saved in this preview."); }}>Save snapshot</Button>
        <Button variant="secondary" disabled={saved === null} onClick={() => {
          if (saved === null) return;
          setInitialState(structuredClone(saved)); setVersion(value => value + 1); setStatus("Saved snapshot restored.");
        }}>Restore snapshot</Button>
      </div>
      <p role="status" className="m-0 text-sm">{status}</p>
      <pre aria-label="Current renderer state" className="m-0 overflow-x-auto text-xs">{JSON.stringify(state, null, 2)}</pre>
    </div>
  );
}

```

Everything here is local. The snapshot lasts only while this preview is mounted. The state shown below the controls is the map emitted by `onStateUpdate`.

## Declare defaults and updates [#declare-defaults-and-updates]

This is the program used by the example:

```text
$quantity = 1
root = Stack([TextContent("Example packing count: " + $quantity), buttons])
buttons = Buttons([Button("Add one", @Set($quantity, $quantity + 1), "primary"), Button("Reset count", @Reset($quantity), "secondary")])
```

The declaration establishes the initial value. `@Set` updates it and `@Reset` returns it to that default. These updates happen in the renderer; they do not generate a new answer or call a model.

## Capture the state map [#capture-the-state-map]

Pass `onStateUpdate` to receive state changes. Store that emitted map with the response, then pass it back as `initialState` when mounting a restored renderer.

```tsx
"use client"

import { Renderer, honestLibrary } from "@/lib/generative"

export function RestoredResponse({ response, state, saveState }: {
  response: string
  state: Record<string, unknown>
  saveState: (state: Record<string, unknown>) => void
}) {
  return (
    <Renderer
      response={response}
      library={honestLibrary}
      initialState={state}
      onStateUpdate={saveState}
    />
  )
}
```

`saveState` is supplied by your application. The example does not choose a database or make a save request for you. When switching to another saved response, mount a renderer for that response so its initialization uses the correct snapshot.

Reactive entries use `$`-prefixed keys. Form entries are grouped by form name and field name, with a `value` and `componentType`. Preserve the emitted structure instead of flattening it into a list of visible input values.

## Build a validated form [#build-a-validated-form]

Use `Form` to group fields, `FormControl` to associate each field with a label and hint, and `Buttons` for its actions. This example requires a valid email:

```text
root = Stack([form])
form = Form("contact", buttons, [emailField])
emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }), "Use a valid email address.")
buttons = Buttons([Button("Send", @ToAssistant("Send contact form"), "primary")])
```

Select **Form** in the [renderer playground](/docs/generative#try-it-locally). Submit an empty value, then a valid email. Invalid input stays in place with a field error; valid input emits an action that the playground displays locally.

The form name is `contact` and its field name is `email`. Those names identify entries in the state map. The action carries form context; inspect the event before connecting it to your server's input format.

## Submit through your application [#submit-through-your-application]

`onAction` receives the generated action. A continuation action asks for another assistant turn; it is not a database write. To save the submitted values, implement that operation in your handler and show its pending, success, and failure states.

Client validation helps the user correct input. Validate it again and check authorization on the server. Keep the form state when a save fails so a retry does not require re-entering every field.

During generation, set `isStreaming` so fields and generated actions remain disabled. Enable them when generation settles. If the stream failed, show that failure even if a partial form is visible.
