# Form, Please Typed, schema-validated React forms that keep native HTML semantics and your design system.
Form, Please mascot holding a coffee mug
# Build forms with ease
Use "Form, Please" to build product forms quickly from an existing design system. Wrap product controls and structural slots once, then reuse them across generated or manually composed forms. It handles state, validation, and submission while your application keeps control of components, markup, styling, and product logic. Start with native controls or bring your own design system.
[Build your first form](/get-started), explore the [live examples](/examples), or review the [API](/api). ## See it work Edit the profile and submit it. The React Hook Form resolver validates and transforms the input once, then "Form, Please" displays the typed output. The live overview form runs only in a browser. It renders an explicit profile definition, validates it with Standard Schema, and returns typed output. Source: [docs-site/src/components/overview-demo.client.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/components/overview-demo.client.tsx) ## What stays explicit * Your Standard Schema owns validation and transformed submit output. * Your definition owns fields, sections, arrays, labels, and conditional UI. * Your controls and slots own the rendered design system. * React Hook Form owns editable state and its low-level API. Use RHF `register`, `Controller`, and hooks such as `useWatch` when a generated definition is not enough. `kit.Form` supplies `FormProvider`. ## Supported package entries The package has seven JavaScript entries: `form-please`, `form-please/default-slots`, `form-please/history`, `form-please/native-controls`, `form-please/persistence`, `form-please/preset-native`, and `form-please/preset-mui`. Release automation controls the exact version. Breaking public behavior starts a new major release line. ## Kudos Kudos to [Evgeniy Ivaha](https://github.com/ivahaev) for the idea and the example implementation. # Get started Build and submit a working form in three steps. This example uses the native preset, Zod, and the default validation timing. ## Install Install "Form, Please", React Hook Form, and a Standard Schema library. This example uses Zod. ```sh npm install form-please react-hook-form zod ``` React 18 and React 19 are supported peer dependencies. ## 1. Define the data contract Create a Standard Schema for the editable input. This schema keeps the editable values unchanged. It adds `slug` only to the successful submit output. ```tsx // [!include ~/snippets/profile-form.tsx:schema] ``` The schema input is `{ name: string; email: string }`. The schema output also contains `slug`. Generated fields always edit the schema input. ## 2. Define the generated fields Connect each schema path to a control from the native preset: ```tsx // [!include ~/snippets/profile-form.tsx:definition] ``` `path` must exist in the schema input. The selected control must accept the value at that path. `required: true` changes the UI state only. The Zod schema still owns the validation rule. ## 3. Bind and render the form Create the form binding inside a React component. Supply one complete schema input as `defaultValues`. ```tsx // [!include ~/snippets/profile-form.tsx:component] ``` `kit.AutoForm` renders the native `
`, an error summary, and the generated fields. Add `kit.Submit` as a child to render the submit button. After a valid submit, `value` contains the schema output, including `slug`. Use the `input` callback property when you also need the editable input. All JavaScript package entries are React client modules. In a Next.js App Router project, put `"use client"` at the start of the component file.
Copy the complete first form ```tsx twoslash // [!include ~/snippets/profile-form.tsx] ```
## Add the optional layout Import the optional structural stylesheet once if you want baseline spacing: ```ts import "form-please/layout.css" ``` The native preset is intentionally unstyled. Replace it with your own controls and slots, or use the Material UI preset. ## Try the interactive lab The lab adds conditional fields, an editable contact array, reset, live React Hook Form state, and a diagnostic snapshot of native browser `FormData`. Submission uses the React Hook Form values, not that snapshot. Submit the form to see the transformed output. The Interactive 'Form, Please' Lab runs only in a browser. It demonstrates a generated form, React Hook Form validation, reset, array actions, watched state, and a diagnostic native FormData snapshot. Submission uses React Hook Form values. Source: [docs-site/src/components/interactive-lab.client.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/components/interactive-lab.client.tsx) Its form kit uses `controls: createNativeControls()` and `slots: createDefaultSlots()`.
Copy the complete interactive lab form ```tsx twoslash // [!include ~/snippets/lab-profile-form.tsx] ```
# Use with AI agents Install the Form, Please skill in a project that uses the library. The skill tells a compatible coding agent to read the current documentation before it implements, reviews, debugs, or explains Form, Please code. The skill follows the open [Agent Skills](https://agentskills.io/) format. It provides instructions to your agent. It does not install the Form, Please npm package or change your application. ## Install the skill Run this command from your project directory: ```sh npx skills add r13v/form-please --skill form-please ``` Follow the CLI prompts to select your agent and installation method. Keep the default project scope when only this project uses Form, Please. Add `--global` to make the skill available in all your projects: ```sh npx skills add r13v/form-please --skill form-please --global ``` The command downloads the skill from the public [Form, Please repository](https://github.com/r13v/form-please/tree/main/skills/form-please). Review `SKILL.md` before you install or update it because your agent follows the instructions in that file. ### Install without the CLI Copy the complete `skills/form-please` directory from the repository to a skill directory that your agent supports. For agents that support the shared project convention, use this path: ```text .agents/skills/form-please/SKILL.md ``` Check your agent documentation if it uses a different skill directory. ## Use the skill A compatible agent can load the skill when your request concerns Form, Please. Name the skill in your request when you want to activate it explicitly. You do not need a product-specific slash command. For an implementation task, use a request such as: ```text Use the form-please skill. Build a profile form with Zod and the native preset. Keep the existing design system. ``` For a review task, use a request such as: ```text Use the form-please skill. Review this form definition for invalid schema paths, validation mistakes, and submission errors. ``` When the skill activates, it tells the agent to read the current [LLM documentation index](https://r13v.github.io/form-please/llms.txt) and use that documentation as its source of truth. This prevents the agent from relying only on information from its training data. ## Verify the installation List the installed skills: ```sh npx skills list ``` Confirm that `form-please` appears for the agent that you selected. Start a new agent session if the current session does not discover the new skill. Then send one of the example requests. If your agent shows its activity, confirm that it reads `SKILL.md` and the LLM documentation before it changes code. ## Update the skill Update a project installation with this command: ```sh npx skills update form-please ``` Add `--global` if you installed the skill globally. # Form kits A form kit is the rendering contract between Form, Please and your project's UI. Create one kit for the controls, structure, and grid that forms in the project share. | Part | Responsibility | Example | | --- | --- | --- | | Control | Connect one typed value to one interactive component. | An email input, checkbox, or date picker. | | Slot | Render structure around controls and groups. | A field wrapper, section grid, or submit button. | ## Choose a starting point Do not build a custom kit when a shipped preset already matches the project. Choose the smallest starting point that covers the UI: | Project need | Start with | What you own | | --- | --- | --- | | Semantic HTML with project CSS | `nativeFormKit` from `form-please/preset-native` | Styles only. | | Native controls with some project components | `createNativeControls()` and `createDefaultSlots()` | Only the controls or slots that you replace. | | A project design system | `defineControl()` and `FormKitSlots` | The interactive components and form structure. | | Material UI 9 | `createMuiFormKit()` from `form-please/preset-mui` | The Material UI theme and field definitions. | The following walkthrough uses the second path. It keeps the accessible default structure, adds one project control, and produces a kit that can render a form. ## Build a project kit ### 1. Define one project control Use `defineControl()` to define the control contract. `Value` must match the schema input type for each field that uses the control. `OwnProps` describes application-owned props for one field. `Context` supplies shared runtime data. Set `Option` only when the control displays a selectable collection. ```tsx twoslash // [!include ~/snippets/form-kits-control.tsx] ``` The example uses `undefined` for an empty value. This is a control decision. Choose an empty value that matches the schema input type. ### 2. Assemble and export the kit Start with the shipped controls and slots. Add project controls to the registry before calling `createFormKit`: ```tsx // [!include ~/snippets/form-kits.tsx:register-control] ``` Create the kit once at module scope and import that instance wherever forms are defined or rendered. `createFormKit` freezes snapshots of the registries and grid; a finished kit does not have a runtime extension API. ### 3. Define fields with the kit The control registry keys become the allowed `control` values. TypeScript checks the schema path, control value type, props, and options together: ```tsx // [!include ~/snippets/form-kits.tsx:control-options] ``` This definition uses the project `uppercase` control together with the shipped `number`, `select`, and `checkbox` controls. ### 4. Bind and render a form Use the same kit to create the form binding and render its generated UI: ```tsx // [!include ~/snippets/form-kits.tsx:project-form] ``` `projectKit.AutoForm` renders the form and generated fields. `projectKit.Submit` uses the kit's submit slot. After validation succeeds, `onSubmit` receives the schema output in `value`. All JavaScript package entries are React client modules. In a Next.js App Router project, keep the kit and the component that calls `useForm` behind a `"use client"` boundary. ## Check the kit before adopting it 1. Put the shared kit in one project module and import the same instance in forms. 2. Confirm that TypeScript rejects a control whose value type does not match its schema path. 3. Submit an invalid form and confirm that the first invalid control receives focus. 4. Confirm that labels, descriptions, and visible errors are announced by a screen reader. 5. Test disabled and readonly fields, plus add, move, and remove actions when the kit supports arrays. The project kit is now usable. The remaining sections document the complete control and slot contracts for replacing more of the baseline. ## Controls A control receives a field value from React Hook Form. It renders the interactive component and sends typed values back to the form. It does not render the field label, description, or errors; the `Field` and `ErrorMessage` slots render that content. ### Control props `ControlProps` contains these properties: | Property | Purpose | | --- | --- | | `path` | The field path in the schema input. | | `value` | The current React Hook Form value. | | `setValue(value)` | Store a new typed value. Do not pass a browser event. | | `blur()` | Mark the field as blurred. Call it from the component blur event. | | `input` | IDs, the field name, and the focus ref for the interactive element. | | `meta` | Validation and interaction state for the field. | | `props` | The readonly application-owned props from the definition. | | `options` | The current readonly selectable collection, for option-capable controls. | | `context` | The deeply readonly context from `kit.useForm`. | | `disabled` | Prevent all interaction when this value is `true`. | | `readOnly` | Show the value but prevent changes when this value is `true`. | | `required` | Expose the resolved UI requirement to the component. | Connect every applicable `input` property to the interactive element: | Input property | Required connection | | --- | --- | | `input.id` | Set the element `id`. The field label uses this ID. | | `input.name` | Set the element `name`. This also supports autofill and browser diagnostics. | | `input.ref` | Attach it to the focusable element. Invalid submission uses this ref for focus. | | `input["aria-describedby"]` | Set `aria-describedby`. It connects descriptions and visible errors. | Set `aria-invalid` from `meta.invalid`. The value becomes `true` only when the form displays field errors. The remaining metadata has these meanings: | Metadata | Meaning | | --- | --- | | `dirty` | The user modified the field value. | | `touched` | The user interacted with the field. | | `validating` | React Hook Form is validating the field. | | `errors` | All normalized field issues. | | `displayErrors` | Issues that the form currently shows. | | `invalid` | `displayErrors` contains at least one issue. | Native text inputs support a `readOnly` property. Some components do not. A custom select, checkbox, or file control must prevent changes when `readOnly` is `true`. ### Option controls Mark the value written by a reusable option item with `OptionValue`. Declare that item as the fourth `defineControl` type argument. The marker has no runtime representation. ```tsx // [!include ~/snippets/types-guide.tsx:choice-control] ``` The control keeps the broad `string` contract. When a form binds it to a schema path such as `"admin" | "member"`, the field definition accepts only those IDs. For an array field, it uses the array element union. Nested option objects are not searched for choice collections. Use `as const` or `satisfies` when options are declared outside the field node. Otherwise, TypeScript can widen a literal ID such as `"admin"` to `string`. Read [Option values](/definitions#option-values) for definition examples. ### Shipped native controls `createNativeControls()` returns a fresh, frozen registry. | Control | Value type | Props | | --- | --- | --- | | `text` | `string \| undefined` | `type`, `placeholder`, `autoComplete` | | `textarea` | `string \| undefined` | `placeholder`, `autoComplete`, `rows` | | `number` | `number \| undefined` | `min`, `max`, `step`, `placeholder` | | `date` | `string \| undefined` | `min`, `max` | | `time` | `string \| undefined` | `min`, `max`, `step` | | `select` | `string \| undefined` | Optional `emptyOption`; items come from field `options`. | | `checkbox` | `boolean` | No props. | | `file` | `File \| undefined` | `accept` | The `select` control requires field `options`. Add `props.emptyOption` when the current value can be `undefined`. Do not combine `emptyOption` with an item whose value is an empty string. Import option types from `form-please/native-controls`. Use `nativeFormKit` from `form-please/preset-native` when you need the complete native baseline. ### Material UI controls `createMuiFormKit()` supplies these controls: | Controls | Value type | Props type | | --- | --- | --- | | `text`, `textarea`, `password`, `email`, `url`, `tel`, `search` | `string \| undefined` | `MuiTextFieldProps` | | `date`, `time`, `datetime-local` | `string \| undefined` | `MuiTextFieldProps` | | `number` | `number \| undefined` | `MuiTextFieldProps` | | `select` | `string \| undefined` | `MuiSelectProps` | | `select-multiple` | `readonly string[]` | `MuiSelectMultipleProps` | | `radio` | `string \| undefined` | `MuiRadioProps` | | `autocomplete` | `string \| undefined` | `MuiAutocompleteProps` | | `autocomplete-multiple` | `readonly string[]` | `MuiAutocompleteMultipleProps` | | `checkbox` | `boolean` | `MuiCheckboxProps` | | `switch` | `boolean` | `MuiSwitchProps` | | `file` | `File \| undefined` | `MuiFileProps` | | `files` | `readonly File[]` | `MuiFileProps` | | `slider` | `number` | `MuiSliderProps` | | `range-slider` | `readonly number[]` | `MuiRangeSliderProps` | Import these props types from `form-please/preset-mui`. The props types omit props that the kit owns, such as the current value, field name, disabled state, and input ref. The field `options` accepted by select, radio, and autocomplete controls narrow to the selected schema path. This rule also applies to their multiple-value variants. Custom select children and autocomplete `freeSolo` values are application-owned and do not participate in option-value inference. ### Value and submission rules Controls store schema input values in React Hook Form. They do not serialize values for submission. A field name or a browser `FormData` value does not change the submitted result. After validation succeeds, `onSubmit` receives the schema output, schema input, form binding, and submitter snapshot. Read [Validation and submission](/validation) for the complete sequence. Read [Product workflows](/workflows#understand-submitter) for named submit actions. ## Slots Slots own the form structure. A complete `FormKitSlots` registry contains six React components. | Slot | Renders | Important props | | --- | --- | --- | | `Field` | A label, description, control, and field errors. | `rootProps`, `labelProps`, `descriptionProps`, `control`, `errors` | | `Section` | A section heading and child layout. | `rootProps`, `layoutProps`, `title`, `description`, `children` | | `Array` | An array label, errors, items, and add action. | `canAdd`, `add`, `children`, `errors` | | `ArrayItem` | One array item and its move or remove actions. | `index`, `canMoveUp`, `canMoveDown`, `move`, `remove` | | `ErrorMessage` | One normalized form issue. | `rootProps`, `issue` | | `Submit` | The kit submit button. | `buttonProps`, `values`, `isSubmitting` | `Field`, `Section`, and `Array` also receive typed `slotOptions`. The form definition supplies these options for one node. ### Slot prop reference The slot types contain these props: | Props | Slots | Purpose | | --- | --- | --- | | `rootProps` | `Field`, `Section`, `Array`, `ArrayItem`, `ErrorMessage` | Attributes and refs for the slot root. | | `slotOptions` | `Field`, `Section`, `Array` | Readonly options from the current definition node. | | `label`, `labelProps` | `Field`, `Array` | Optional label content and its generated attributes. | | `description`, `descriptionProps` | `Field`, `Array` | Optional description content and its generated attributes. | | `control` | `Field` | The rendered control component. | | `errors` | `Field`, `Array` | Rendered `ErrorMessage` slot elements for visible issues. | | `disabled`, `readOnly`, `required` | `Field` | Resolved interaction requirements for the field. | | `title`, `description` | `Section` | Optional section content. | | `layoutProps` | `Section` | Grid attributes for the child layout element. | | `invalid` | `Array` | Whether the array has visible issues. | | `canAdd`, `add()` | `Array` | Add-action state and callback. | | `index` | `ArrayItem` | The zero-based item index. | | `disabled`, `readOnly` | `ArrayItem` | Resolved interaction state for the item. | | `canMoveUp`, `canMoveDown` | `ArrayItem` | Boundary and interaction state for move actions. | | `move(toIndex)`, `remove()` | `ArrayItem` | Array mutation callbacks. | | `children` | `Section`, `Array`, `ArrayItem` | Rendered child nodes or items. | | `issue` | `ErrorMessage` | One normalized `FormIssue`. | | `buttonProps` | `Submit` | Button content, attributes, submit type, and resolved disabled state. | | `values` | `Submit` | The current form input object. | | `isSubmitting` | `Submit` | Whether the submit handler has a pending promise. | ### Implement a field slot Spread generated props on the matching elements. These props preserve labels, descriptions, focus behavior, CSS hooks, and resolved state. ```tsx // [!include ~/snippets/form-kits.tsx:field-slot] ``` Render `control` exactly once. Render each item in `errors` so users can read the displayed validation issues. ### Implement an array slot Call `add()` from a button with `type="button"`. Disable the action when `canAdd` is `false`. The kit sets `canAdd` to `false` for disabled and readonly arrays. ```tsx // [!include ~/snippets/form-kits.tsx:array-slot] ``` The `ArrayItem` slot owns move and remove controls. Use `canMoveUp` and `canMoveDown` for move state. Also disable all item actions when `disabled` or `readOnly` is `true`. ### Implement a submit slot Spread `buttonProps` on the button. These props contain `type="submit"` and the resolved disabled state. ```tsx // [!include ~/snippets/form-kits.tsx:submit-slot] ``` Use `isSubmitting` for pending content. Use `values` only when the button UI depends on the current form input. ### Build the slot registry `createFormKit` requires all six slots. Start with `createDefaultSlots()` when you only need to replace some slots. ```tsx // [!include ~/snippets/form-kits.tsx:slot-registry] ``` The generic parameters of `FormKitSlots` define options for `Field`, `Section`, and `Array`, in that order. Use `never` when a slot does not accept options. Pass the typed options through `slotOptions` in the form definition: ```tsx // [!include ~/snippets/form-kits.tsx:slot-options] ``` ### Generated structural props `rootProps` contains stable attributes for styling and state inspection. Always spread it on the slot root element. | Attribute | Meaning | | --- | --- | | `data-fp-node` | The structural node type. | | `data-fp-path` | The current field or array path, when applicable. | | `data-fp-span` | The resolved grid span, when applicable. | | `data-invalid`, `data-dirty`, `data-touched` | Current validation and interaction state. | | `data-disabled`, `data-readonly`, `data-required` | Current interaction requirements. | | `data-validating` | The node is validating. | `Section.layoutProps` contains `data-fp-layout="grid"` and `data-fp-columns`. Spread it on the element that arranges the section children. Import `form-please/layout.css` for optional responsive structure. Read [Styling](/styling) for the CSS contract. ### Shipped slot sets Use `createDefaultSlots()` for accessible HTML structure without a visual theme. Its `i18n` option changes array action labels. Use `createMuiFormKit()` for Material UI 9 controls, slots, and a 12-column grid. The application still owns the Material UI theme. Read the [API reference](/api#native-controls-and-default-slots) for factory imports and localization types. # Definitions A definition connects one Standard Schema to the UI that edits its input. Call `kit.defineForm(schema, (ui) => [...])` outside the component that uses the form. The schema-bound helpers keep paths, controls, options, and resolver values connected while hiding the runtime node discriminator. ```tsx import { nativeFormKit } from "form-please/preset-native" import { z } from "zod" const profileSchema = z.object({ name: z.string().min(1), email: z.email(), company: z.string().optional(), contactByEmail: z.boolean(), }) const profileDefinition = nativeFormKit.defineForm(profileSchema, (ui) => [ ui.section("contact", { title: "Contact details", columns: 2, children: [ ui.field("name", { control: "text", label: "Name" }), ui.field("email", { control: "text", label: "Email", props: { type: "email", autoComplete: "email" }, }), ui.field("company", { control: "text", label: "Company", span: "full", }), ui.field("contactByEmail", { control: "checkbox", label: "Contact me by email", span: "full", }), ], }), ]) ``` The type checker verifies these connections: * Each `path` exists in the schema input. * Each control accepts the value at its field path. * Control props, selectable options, and slot options use their registered types. * Resolver context matches the kit context. * Array defaults match one complete array item. * `columns` and `span` use the kit grid. `defineForm` also validates the tree at runtime. It rejects unknown controls, invalid path syntax, duplicate IDs, missing children, and invalid grid values. The object form `defineForm(schema, { ui: [...] })` remains supported for generated definitions and direct node composition. Both authoring styles produce the same normalized tree. ## Fields A field connects one schema input path to one registered control. The path type comes from the schema input, not from a transformed schema output. ```tsx const schema = z.object({ age: z.string().transform(Number), }) const definition = nativeFormKit.defineForm(schema, (ui) => [ ui.field("age", { control: "text", // The input value is a string. label: "Age", description: "Enter your age in years.", props: { placeholder: "42" }, required: true, }), ]) ``` `control` selects a control from the kit registry. `props` supplies application-owned props to that control instance. Only controls that display a selectable list accept `options`. The `required` property describes the UI state. It does not add a validation rule. Put all data rules in the schema. ### Option values A selectable control can mark the value it writes with `OptionValue`. The field definition then narrows each option to the Standard Schema input type at its `path`; the form does not need a separate value generic. ```tsx const roleSchema = z.object({ role: z.enum(["admin", "member"]), }) const roleOptions = [ { value: "admin", label: "Administrator" }, ] as const nativeFormKit.defineForm(roleSchema, (ui) => [ ui.field("role", { control: "select", options: [ ...roleOptions, { value: "owner", label: "Owner" }, // ^^^^^^^ TypeScript rejects this value. ], }), ]) ``` The list may contain any subset of the field union; it does not have to be exhaustive. A `Role | undefined` field still accepts only `Role` as a normal option, while the native `props.emptyOption` represents `undefined`. A `readonly Role[]` field narrows options to the element union. A plain `string` field continues to accept arbitrary strings. The same restriction applies when `options` is a function. Preserve literal types in context or resource data with `as const`, `satisfies`, or another explicit union type. A widened `string[]` cannot supply options for a literal schema union. ## Sections A section groups related nodes. It can contain fields, arrays, render nodes, and other sections. A section does not change the path scope of its children. ```tsx ui.section("delivery-address", { title: "Delivery address", description: "We use this address for the current order.", columns: 2, children: [ ui.field("delivery.street", { control: "text", label: "Street", span: "full", }), ui.field("delivery.city", { control: "text", label: "City", }), ui.field("delivery.postcode", { control: "text", label: "Postcode", }), ], }) ``` `columns` sets the grid for the section children. The default kit grid accepts `1`, `2`, `3`, and `4`. A custom kit can define a different grid. `span` must use a grid value that does not exceed the parent columns. Use `"full"` to span all parent columns. Import `form-please/layout.css` to use the optional responsive grid. Custom slots can implement the same layout contract. See [Styling](/styling). ## Reusable fragments `kit.defineFragment(schema, (ui) => [...])` defines one schema-owned UI subtree and keeps the concrete schema available as `fragment.schema`. The object form `defineFragment(schema, { ui })` remains supported. Compose that schema into a form schema, then place the fragment at each compatible object path: ```tsx // [!include ~/snippets/api-reference.tsx:form-fragment] ``` `fragment.fields({ at })` creates one opaque authoring placement. `defineForm` expands it into ordinary fields, sections, arrays, and render nodes. The fragment does not add runtime state, a validation pass, or a lifecycle. It must come from the same runtime kit as the host definition. The `at` path is relative to the current form or array-item scope. Its value must structurally satisfy the fragment schema input; additional object properties are allowed, while optional or incompatible objects are rejected. Call `fragment.fields()` without `at` when the current scope itself satisfies the fragment input, such as an array whose items are addresses. Use `kit.forContext()` before `defineFragment` to declare the minimum context required by the fragment. A host context may provide additional properties. Resolvers inside the fragment receive the local deeply readonly fragment input and its required context. Put conditions that need the complete host input on a section or another ordinary node around the placement. ## Arrays An array node connects an array path to the definition for one item. Paths in `children` are relative to that item. ```tsx const eventSchema = z.object({ speakers: z.array( z.object({ name: z.string(), sessions: z.array(z.object({ title: z.string() })), }), ), }) const eventDefinition = nativeFormKit.defineForm(eventSchema, (ui) => [ ui.array("speakers", { label: "Speakers", itemDefault: () => ({ name: "", sessions: [] }), children: (speaker) => [ speaker.field("name", { control: "text", label: "Name" }), speaker.array("sessions", { label: "Sessions", itemDefault: { title: "" }, children: (session) => [ session.field("title", { control: "text", label: "Title", }), ], }), ], }), ]) ``` Generated field paths use React Hook Form dot syntax. The title above can resolve to `speakers.2.sessions.0.title`. `itemDefault` accepts a complete item or a function that returns one. Form, Please clones the result before each insertion. Use a function when the default needs a new value, such as a generated ID. This function takes no arguments and is not a UI resolver. The `children` callback runs once while the definition is created. Its builder is bound to the array item, so each nested path is relative to that item. It is not a resolver and does not run for each rendered row. Array paths use their current index, while RHF supplies a stable row key. Moving or removing a row changes later paths. Put a durable ID in the schema when identity must survive serialization or remounting. See [Arrays](/arrays) for custom array actions. ## Resolver values Display, interaction, control props, and layout properties accept either a static value or a synchronous resolver. ```tsx const accountSchema = z.object({ accountType: z.enum(["business", "nonprofit", "personal"]), company: z.string().optional(), }) const accountKit = nativeFormKit.forContext<{ readonly canEditCompanies: boolean }>() const accountDefinition = accountKit.defineForm(accountSchema, (ui) => [ ui.field("company", { control: "text", label: (values) => values.accountType === "business" ? "Company" : "Organization", visible: (values) => values.accountType !== "personal", disabled: (_values, { context }) => !context.canEditCompanies, props: (values) => ({ placeholder: values.accountType === "nonprofit" ? "Foundation name" : "Company name", }), }), ]) ``` For ordinary definition nodes, the first argument is the complete, deeply readonly schema input. This rule also applies inside array items. Resolvers authored by a reusable fragment receive that fragment's local input instead. The second argument contains the deeply readonly runtime `context` from `kit.useForm`. ### Async options The `options` collection is the one property that may load asynchronously. Its function receives one object, and the runtime tracks the `values` and `context` properties it reads: ```tsx ui.field("city", { control: "select", props: { emptyOption: { label: "Choose a city" } }, options: async ({ values, context, signal }) => { const response = await fetch( `/api/cities?country=${values.country}&locale=${context.locale}`, { signal }, ) return response.json() }, }) ``` Changing `values.country` or `context.locale` aborts the previous request and runs the function again. Unrelated changes do not. The rendered list is empty during loading and after an uncaught error; catch inside the function when you want another fallback. The field value is preserved even when it is absent from the current list. A successfully resolved value must be an array; any other value is a resolver contract error surfaced through React. Use an application resource or a custom control when you need loading or error UI, caching, retries, remote search, or pagination. Resolvers must return immediately. A resolver that returns a promise causes a runtime error. Keep requests and cache state in the application. Pass their current result through context. Form, Please resolves the complete UI tree after each form value change. It does not track resolver dependencies or cache resolver results. Keep resolvers small and free of side effects. Object and array results are compared shallowly when Form, Please reuses unchanged UI branches. Treat resolver results as immutable and replace nested values when their content changes. See [Conditional fields](/conditional-fields) for conditional validation and value cleanup. ## Visibility and interaction state The default values are `visible: true`, `disabled: false`, `readOnly: false`, and `required: false`. A section or array passes its resolved state to all descendants. ```tsx { kind: "section", id: "billing", disabled: (values) => values.useSavedBillingAddress, children: [ { kind: "field", path: "billingAddress.street", control: "text", label: "Street", }, ], } ``` When a parent is disabled or read-only, a child cannot override that state. When a parent is hidden, Form, Please does not render its descendants. Hidden fields keep their React Hook Form values. These properties control the UI only. They do not change the schema or remove values from submission. ## Node property reference These properties apply to more than one node type: | Property | Node types | Purpose | | --- | --- | --- | | `id` | All | Identifies the node. It is required for sections and render nodes. | | `visible` | All | Shows or hides the node. The default is `true`. | | `disabled` | All | Marks the node disabled. The default is `false`. | | `readOnly` | All | Marks the node read-only. The default is `false`. | | `className` | Field, section, array | Adds a class to the structural root. | | `span` | Field, section, array | Sets the node width in its parent grid. | | `slotOptions` | Field, section, array | Passes typed options to the matching slot. | Field properties: | Property | Required | Purpose | | --- | --- | --- | | `kind: "field"` | Yes | Selects a field node. | | `path` | Yes | Selects a path in the current schema scope. | | `control` | Yes | Selects a compatible registered control. | | `label` | No | Supplies string or React label content. | | `description` | No | Supplies string or React help content. | | `props` | Control-dependent | Supplies application-owned props to the selected control. | | `options` | Option-capable controls | Supplies a static or asynchronously loaded selectable list. | | `required` | No | Passes UI requirement state to the control. | Section properties: | Property | Required | Purpose | | --- | --- | --- | | `kind: "section"` | Yes | Selects a section node. | | `id` | Yes | Gives the section a stable identifier. | | `children` | Yes | Contains nodes in the same path scope. | | `title` | No | Supplies string or React heading content. | | `description` | No | Supplies string or React help content. | | `columns` | No | Sets the child grid. The default is `1`. | Array properties: | Property | Required | Purpose | | --- | --- | --- | | `kind: "array"` | Yes | Selects an array node. | | `path` | Yes | Selects an array path in the current schema scope. | | `itemDefault` | Yes | Supplies one complete item for the add action. | | `children` | Yes | Contains nodes relative to one array item. | | `label` | No | Supplies string or React label content. | | `description` | No | Supplies string or React help content. | Render properties: | Property | Required | Purpose | | --- | --- | --- | | `kind: "render"` | Yes | Selects a render node. | | `id` | Yes | Gives the render node a stable identifier. | | `component` | Yes | Supplies the React component to render. | `label`, `description`, `title`, `props`, `slotOptions`, `visible`, `disabled`, `readOnly`, `required`, `className`, `columns`, and `span` can use resolvers where their node type supports the property. See [Form kits](/form-kits) for control props, selectable options, slot options, and custom slot examples. ## IDs and paths Sections and render nodes require an explicit `id`. Fields and arrays derive an ID from their path when you omit `id`. IDs must be non-empty and unique in the same path scope. Use RHF dot syntax for both object properties and array indexes: ```tsx { kind: "field", path: "profile.email", control: "text" } { kind: "field", path: "speakers.0.name", control: "text" } ``` Do not put a root path in an array child. Use the path relative to one item: ```tsx { kind: "array", path: "speakers", itemDefault: { name: "" }, children: [ // Correct: resolves to speakers.0.name for the first item. { kind: "field", path: "name", control: "text" }, ], } ``` ## Form-local React content A render node inserts application content into the generated tree. Its component receives the resolved inherited `disabled` and `readOnly` flags. ```tsx // [!include ~/snippets/api-reference.tsx:render-node] ``` A render node also accepts resolver-based `visible`, `disabled`, and `readOnly`. It requires an explicit `id` and a `component`. Use direct RHF composition outside `kit.Fields` when content needs the form API. Render nodes deliberately receive only interaction state. ## Definition lifetime Keep a definition stable for the complete `kit.useForm` hook lifetime. Define it at module scope when possible. ```tsx const definition = kit.defineForm(schema, { ui }) function Editor() { const form = kit.useForm(definition, { defaultValues }) return } ``` Passing a different definition to an existing hook does not replace its first definition. When the definition must change, remount the component with a React `key`. # Validation and submission Use the definition schema as the single form-level validation contract. The schema validates the complete React Hook Form input. It also produces the value that `onSubmit` receives. ## Build the validation schema Put field rules, cross-field rules, and transforms in the Standard Schema. This Zod schema validates reserved seats against the total capacity. It also transforms both seat values from strings to numbers. ```tsx // [!include ~/snippets/validation-guide.tsx:schema] ``` Give a cross-field issue the path of the field that the user must change. Form, Please converts Standard Schema paths to RHF dot paths, such as `contacts.2.email`. ## Connect fields to the schema Pass the schema to `kit.defineForm`. The generated fields edit the schema input. ```tsx // [!include ~/snippets/validation-guide.tsx:definition] ``` The `required` property changes the rendered control and its accessibility state. It does not add a schema rule. The schema must reject a missing value. `kit.Form` sets `noValidate` on the native form element. Browser constraints, such as `required` and `type="email"`, do not replace schema validation. ## Understand validation timing The resolver validates through React Hook Form once per validation run. Form, Please configures this validation sequence: | User action | Result | | --- | --- | | Edit before the first submit | The form does not run change validation. | | Submit for the first time | React Hook Form validates the complete schema input. | | Edit after the first submit attempt | React Hook Form validates on each change. | | Submit valid input | The resolver returns transformed output and "Form, Please" calls `onSubmit`. | The first submit attempt can be valid or invalid. After that attempt, each edit runs the complete form-level schema. `useForm` can override `mode`, `reValidateMode`, and `delayError`; it does not expose a replacement resolver. ## Display issues Standard Schema issues become public `FormIssue` values: ```ts type FormIssue = { readonly message: string readonly path?: string } ``` A generated control receives all current issues in `meta.errors`. It receives the issues that the UI can show in `meta.displayErrors`. The default field slot shows issues after the field is touched or after a submit attempt. `kit.AutoForm` also renders an error summary. The summary contains issues that do not belong to a visible generated control. It also contains issues for disabled generated controls. Omit the issue path when the user cannot fix the issue in one field: ```tsx // [!include ~/snippets/validation-guide.tsx:form-issue] ``` Use `kit.Form` and `kit.Fields` for manual composition. In that case, the application must render its own summary for form-level and hidden-field issues. ## Focus the first issue After an invalid submit, RHF focuses its first registered invalid field, including an application-owned field. Focus order follows RHF registration order. If no generated control can receive focus, `kit.AutoForm` focuses the first error-summary item. A manually composed `kit.Form` has no automatic summary fallback. Hidden fields keep their values and remain part of schema validation. A hidden field has no generated control, so its issue appears in the `kit.AutoForm` summary. The schema must match the form's visibility rules when a hidden value must not block submission. ## Transformed output The internal RHF resolver validates and produces `FormOutput` in one Standard Schema parse. Editable `FormInput` remains in RHF state; transformed output is passed only to a successful submit callback. No custom validation cache is required. ## Submit input and output `onSubmit` receives four values: * `value` is the transformed `FormOutput`. * `input` is the editable `FormInput`. * `form` is the current form binding. * `submitter` is the captured submit control `name` and `value`, or `null`. This example sends the transformed `value` to the server. It resets the editable baseline with `input` after a successful request. ```tsx // [!include ~/snippets/validation-guide.tsx:submission] ``` `submitter` is captured before validation. It is not a live DOM element and it does not contain request metadata. `onSubmit` can return a promise. `form.api.formState.isSubmitting` remains true until the promise settles. `kit.Submit` is disabled while the form validates or submits. Use `FormSubmitDetails` from `form-please` to type an extracted submit handler without repeating the `{ value, input, form, submitter }` object shape. Read [Product workflows](/workflows#understand-submitter) for multiple submit actions and the complete snapshot contract. Server validation is still required. A request failure is not a Standard Schema issue and does not appear in the form summary. Store and render request failures in application state, as the example does. ## Set default Zod messages "Form, Please" displays the messages that the Standard Schema returns. Configure Zod once, before form validation runs, when the application needs global defaults: ```ts twoslash // [!include ~/snippets/zod-error-messages.ts] ``` Return `undefined` for issue types that must use the Zod default. A schema-level message has priority over `customError`. Do not call `z.config()` during a component render because it changes global Zod configuration. # Styling Form, Please separates layout, structural markup, and control markup. Choose the narrowest styling method that gives you the required result. | Requirement | Use | | --- | --- | | Responsive columns and spacing | Import `form-please/layout.css`. | | Colors, borders, typography, and state styles | Select the generated `data-*` attributes in your CSS. | | A class for one field, section, or array | Set its `className` property. | | Different structural markup | Replace one or more slots. | | Different control markup | Register a custom control. | The native preset uses native HTML controls and the default slots. It does not include a visual theme. The Material UI preset uses Material UI controls and slots. Your application still owns the Material UI theme. ## Add the responsive layout Import the layout CSS once in your application entry point or root layout: ```tsx import "form-please/layout.css" import "./forms.css" ``` The import is optional. It adds only structural grid and spacing rules. It does not set colors, typography, borders, or control appearance. The file uses the `fp` cascade layer and low-specificity `:where()` selectors. Unlayered application CSS overrides these rules. If your CSS uses layers, put your theme layer after `fp`. ### Configure columns and spans Set `columns` on a section. Set `span` on a field, nested section, or array: ```tsx { kind: "section", id: "contact", title: "Contact details", columns: 2, children: [ { kind: "field", path: "email", control: "text", label: "Email", }, { kind: "field", path: "notes", control: "textarea", label: "Notes", span: "full", }, ], } ``` The default grid accepts `1`, `2`, `3`, and `4`. The layout responds to the width of the current section, not the viewport width. The packaged CSS implements only these four column counts. If a custom kit uses other values, its slots or application CSS must implement that grid. The Material UI preset implements its 12-column grid with Material UI. | Section width | Result | | --- | --- | | Less than `40rem` | One column. | | At least `40rem` | Up to two columns. | | At least `64rem` | Up to the requested three or four columns. | A numeric `span` uses the available columns at each width. `span: "full"` always uses the full row. Nested sections respond to their own width. ### Change the layout gaps Set the layout variables on the form or on a structural node. Descendants inherit the values. ```tsx // [!include ~/snippets/styling-guide.tsx:form-root] ``` | Variable | Default | Purpose | | --- | --- | --- | | `--fp-stack-gap` | `0.75rem` | Space between content in forms, fields, sections, and arrays. | | `--fp-array-item-gap` | `1rem` | Space between content in one array item. | | `--fp-column-gap` | `1rem` | Horizontal space in a section grid. | | `--fp-row-gap` | `1rem` | Vertical space in a section grid. | ## Add an application theme Add a class to `kit.Form` or `kit.AutoForm`. Scope your selectors to that class so multiple form themes can exist on one page. ```css .account-form { --fp-stack-gap: 1rem; color: #172033; font: 1rem/1.5 system-ui, sans-serif; } .account-form [data-fp-node="section"] { border: 1px solid #d8deea; border-radius: 0.75rem; padding: 1.25rem; } .account-form [data-fp-node="field"] > label { font-weight: 650; } .account-form [data-fp-node="field"] > :is(input:not([type="checkbox"]), select, textarea) { box-sizing: border-box; width: 100%; border: 1px solid #9aa7bd; border-radius: 0.5rem; padding: 0.625rem 0.75rem; } .account-form [data-fp-node="error-message"] { color: #b42318; font-size: 0.875rem; } ``` This example targets the markup from `createDefaultSlots()` and the native controls. A custom slot or control can use different elements. ### Style one path Use `data-fp-path` when the field path is the stable styling identifier: ```css .account-form [data-fp-node="field"][data-fp-path="email"] { grid-column: 1 / -1; } .account-form [data-fp-node="field"][data-fp-path^="contacts."][data-fp-path$=".email"] { background: #f7f9fc; } ``` Array paths contain the current item index, such as `contacts.0.email`. The index changes when a user moves or removes an item. ## Use structural attributes Generated elements expose stable attributes. Custom slots preserve the contract only when they spread `rootProps` and `layoutProps`. | Attribute | Location and meaning | | --- | --- | | `data-fp-node` | Identifies `form`, `field`, `section`, `array`, `array-item`, or `error-message`. | | `data-fp-path` | Contains the current path on fields, arrays, array items, and path-specific errors. | | `data-fp-span` | Contains the resolved span on fields, sections, and arrays. | | `data-fp-layout="grid"` | Identifies the element that arranges section children. | | `data-fp-columns` | Contains the resolved column count on the section layout element. | The default and Material UI slots also add hooks for array controls: ```css .account-form [data-fp-array-action="add"] { /* Add button */ } .account-form [data-fp-array-action="move-up"] { /* Move-up button */ } .account-form [data-fp-array-action="move-down"] { /* Move-down button */ } .account-form [data-fp-array-action="remove"] { /* Remove button */ } .account-form [data-fp-array-item-actions] { /* Item action group */ } .account-form [data-fp-array-item-position] { /* Visible item position */ } ``` ### Style state Boolean state attributes have an empty value when the state is true. The attribute is absent when the state is false. Test for presence: ```css /* Correct */ .account-form [data-fp-node="field"][data-invalid] { color: #b42318; } /* This selector does not match. */ .account-form [data-invalid="true"] { color: #b42318; } ``` | Attribute | Generated state | | --- | --- | | `data-invalid` | A field or array has validation issues that the form displays. | | `data-dirty`, `data-touched` | A field or array has the matching interaction state. | | `data-validating` | A field or array is validating. | | `data-disabled`, `data-readonly` | The form, field, section, array, or array item has the resolved restriction. | | `data-required` | A field has the resolved required state. | `data-invalid` does not appear for a hidden validation issue. It appears after the field is touched or after the first submit attempt. Native controls also receive `aria-invalid="true"` when their displayed errors make them invalid: ```css .account-form :is(input, select, textarea)[aria-invalid="true"] { border-color: #b42318; outline-color: #b42318; } ``` ## Add static or resolved classes Field, section, and array definitions accept `className`. Use a string for a fixed class. Use a synchronous resolver when the class depends on form input. ```tsx // [!include ~/snippets/styling-guide.tsx:node-classes] ``` The resolver receives the complete form input as its first argument. It receives `{ context }` as its second argument: ```tsx // [!include ~/snippets/styling-guide.tsx:context-class] ``` The kit resolves the class again when form input changes. It puts the result in the node's `rootProps`. A custom slot must spread `rootProps`. If the slot adds a class, it must also preserve `rootProps.className`. ## Resolve classes from form values Change the account type to **Company**. The resolver changes the section classes and shows the company-name field. The live Tailwind profile form runs only in a browser. A synchronous resolver changes its account-section utility classes from the current React Hook Form values. Source: [docs-site/src/components/tailwind-profile-demo.client.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/components/tailwind-profile-demo.client.tsx) Return complete class strings from each branch. Tailwind can then discover all utilities at build time. ```tsx // [!include ~/snippets/lab-profile-form.tsx:tailwind-class-name] ``` Do not construct utility names from fragments such as `bg-${color}-50`. ## Replace structural markup Slots own the markup around controls. They do not own the control markup. Start with `createDefaultSlots()` and replace only the slot that needs different markup. ```tsx // [!include ~/snippets/styling-guide.tsx:custom-field-slot] ``` Spread `rootProps` on the slot root. Spread `labelProps`, `descriptionProps`, and `layoutProps` on their matching elements. These props preserve generated IDs, accessibility relationships, state hooks, and layout hooks. Read [Form kits](/form-kits#slots) for all six slot contracts and complete array and submit examples. ## Style Material UI forms The Material UI preset uses its own responsive `Grid`. Use the application theme for shared styles. Use `props.sx` for a control and `slotOptions` for a field, section, or array. ```tsx { kind: "section", id: "proposal", title: "Conference proposal", columns: 12, slotOptions: { sx: { width: "100%" }, layoutSx: { alignItems: "start" }, }, children: [ { kind: "field", path: "title", control: "text", label: "Proposal title", span: 7, props: { sx: { bgcolor: "background.paper" } }, }, ], } ``` Read [Material UI with Yup](/examples/mui-yup) for a complete themed example. # Conditional fields Use a synchronous resolver when a UI property depends on form values or runtime context. A resolver returns the property value for the current form state. ## Show or hide a field Set `visible` to a resolver that returns a boolean: ```tsx // [!include ~/snippets/lab-profile-form.tsx:conditional-field] ``` When `accountType` changes, "Form, Please" resolves the UI tree again. The `companyName` field appears only for a company account. `visible` has these effects: * A hidden node does not render. * A hidden section or array does not render its descendants. * A hidden field keeps its value in React Hook Form state. * The Standard Schema still receives the hidden value during submission. `visible` does not clear a value or change schema validation. ## Derive field properties Field properties can use the same form values: ```tsx // [!include ~/snippets/conditional-fields-guide.tsx:derived-field] ``` On an ordinary definition node, the first resolver argument is the complete, deeply readonly schema input. A resolver authored inside a reusable fragment receives that fragment's local input instead. It is not only the value at the field path. These field properties accept a static value or a synchronous resolver: | Property | Result | Default | | --- | --- | --- | | `visible` | Shows or hides the field. | `true` | | `disabled`, `readOnly` | Changes the control interaction state. | `false` | | `required` | Passes required UI state to the control. | `false` | | `label`, `description` | Changes displayed content. | No content | | `props`, `slotOptions` | Changes control props or slot configuration. | Control-dependent | Layout properties such as `className` and `span` also accept resolvers. See [Definitions](/definitions#resolver-values) for the complete property reference. Ordinary resolvers must return immediately. A resolver that returns a promise causes a runtime error. Keep resolvers small and free of side effects. Do not fetch data or update form state in an ordinary resolver. A selectable field's `options` property has a separate function contract. It may return a promise and tracks the `values` and `context` properties it reads. See [Async options](/definitions#async-options). ## Apply one condition to a group Put `visible`, `disabled`, or `readOnly` on a section or array when one condition applies to all descendants: ```tsx // [!include ~/snippets/conditional-fields-guide.tsx:conditional-section] ``` A descendant cannot override inherited state: * A hidden parent hides all descendants. * A disabled parent disables all descendants. * A read-only parent makes all descendants read-only. `required` is a field property. It is not inherited from a section or array. ## Use runtime context Use context for data that is not part of the editable form value. Examples include permissions, feature flags, and loaded option lists. Bind the context type once with `kit.forContext()`: ```tsx // [!include ~/snippets/conditional-fields-guide.tsx:runtime-context] ``` The second resolver argument contains the current, deeply readonly `context`. Pass a concrete context value to `useForm`. Keep requests and cache state in the application. Pass their current state through context. See [Resource state](/resources) for `fromResource` examples. ## Use resolvers inside arrays carefully An array-child resolver receives the complete root input. It does not receive the current row or its index. Therefore, the same resolver result applies to each generated row for a given form state. Use an array-node resolver for conditions that depend on the complete array: ```tsx // [!include ~/snippets/conditional-fields-guide.tsx:array-resolver] ``` Keep row and cross-row validation in the Standard Schema. Use custom array UI when the UI condition must depend on the current row index. See [Arrays](/arrays) for custom operations and array slots. ## Keep conditional validation in the schema `visible` and `required` control the rendered UI. They do not add, remove, or change schema rules. Express the same condition in the Standard Schema: ```tsx // [!include ~/snippets/conditional-fields-guide.tsx:conditional-schema] ``` This schema requires `companyName` only for a company account. Its transform also removes a stale `companyName` from personal-account output. Use a schema transform when cleanup is part of the submission contract. Use value middleware when the source and cleared value must commit atomically. Use `form.update` for the same managed boundary in an application action. See [Value middleware](/middleware). # Arrays Use an array node for repeatable object rows with generated child fields. The current form value supplies the rows. The `Array` and `ArrayItem` slots supply the add, remove, and move actions. Use one field control when one widget edits the complete array value. For example, an async multiselect is one field, not a set of repeatable rows. See [Async multiselect](/examples/async-multiselect). ## Define a repeatable row Define the array in the schema, the initial rows, the add-action default, and the child fields: ```tsx // [!include ~/snippets/arrays-guide.tsx:define-array] ``` These four parts have different purposes: | Part | Purpose | | --- | --- | | Schema | Defines the array item and its validation rules. | | `defaultValues` | Supplies the rows that exist when the form starts. | | `itemDefault` | Supplies one new row for the generated add action. | | `children` | Defines the generated UI for one row. | `itemDefault` does not add an initial row. Use `defaultValues` when the form must start with one or more rows. Child paths are relative to one item. In the example, `path: "email"` produces `contacts.0.email`, `contacts.1.email`, and later paths.
See the contact array from the interactive lab ```tsx // [!include ~/snippets/lab-profile-form.tsx:array-node] ```
## Choose an item default Use an object when each new row can use the same data: ```tsx itemDefault: { name: "", sessions: [] } ``` Use a function when each new row needs a unique value: ```tsx itemDefault: () => ({ id: crypto.randomUUID(), name: "", sessions: [], }) ``` The generated add action calls the function for each insertion. It then deeply clones the result while preserving browser values such as `File`. Nested objects in two generated rows do not share references. A manual React Hook Form operation does not use `itemDefault`. Supply the new item when you call `append()` or `insert()` from `useFieldArray`. ## Define nested arrays Put an array node inside another array node. Keep each child path relative to its immediate item: ```tsx // [!include ~/snippets/arrays-guide.tsx:nested-array] ``` The same definition produces these paths at runtime: | Definition path | Example runtime path | | --- | --- | | `speakers` | `speakers` | | `sessions` | `speakers.2.sessions` | | `title` | `speakers.2.sessions.0.title` | Generated paths use RHF dot notation for array indexes. `FieldPath`, `ArrayFieldPath`, and `PathValue` use the same syntax. Generated arrays contain object rows; use one custom control for a primitive array value. ## Validate arrays and rows Keep array length, item, and cross-row rules in the Standard Schema. This Zod example reports a duplicate email on the specific row that must change: ```tsx // [!include ~/snippets/arrays-guide.tsx:array-validation] ``` "Form, Please" converts numeric issue paths to dot notation. The duplicate issue above resolves to a path such as `contacts.2.email`. After an invalid submit, the form focuses the first visible generated control with an issue. Array-level issues remain available through the array slot and the form error summary. See [Validation and submission](/validation). ## Understand the generated actions The default array slots provide these actions: * Add a row at the end. * Move a row up or down. * Remove a row. The generated add action uses `itemDefault`. Move and remove operations update React Hook Form state by index. All three actions enter configured value middleware, which can cancel the action or add dependent value patches. The source operation still uses native `useFieldArray` methods so row IDs survive. Middleware cannot change the source array length or order beyond the proposed generated action. It must not change another generated array's structure. Dependent changes join the same final React render, but raw RHF subscriptions can observe the native array operation before those changes. Use each array's generated action for a managed structural change because `setValues` cannot synchronize private row IDs. Read [Value middleware](/middleware) for transaction sources, cancellation, dependent patches, raw RHF observations, and structural limits. When an array is disabled or read-only, the kit also disables its add, move, and remove actions. Child fields inherit that state. A hidden array keeps its value and does not render. See [Conditional fields](/conditional-fields). ## Add custom array operations Use RHF `useFieldArray({ control: form.api.control, name })` for application-owned operations. This example adds a toolbar after the generated fields: ```tsx // [!include ~/snippets/arrays-guide.tsx:custom-operations] ``` Use these methods to add or update rows: | Method | Result | | --- | --- | | `append(item)` | Adds an item at the end. | | `insert(index, item)` | Adds an item at an index. | | `update(index, item)` | Replaces one item. | Use these methods to reorder or remove rows: | Method | Result | | --- | --- | | `move(from, to)` | Moves one item to another index. | | `swap(a, b)` | Exchanges two items. | | `remove(index)` | Removes one item. | | `replace([])` | Removes all items. | These methods update the same React Hook Form state as the generated actions, but direct RHF operations bypass Form, Please middleware. They do not replace the generated array markup. Customize the `Array` and `ArrayItem` slots when you must replace that markup or its buttons. See [Controls and slots](/form-kits#implement-an-array-slot). ## Preserve row identity RHF supplies a stable `field.id` for each rendered row key. A move or removal still changes indexed public paths. For example, `contacts.2` can become `contacts.1`. Put a durable ID in each item when identity must survive reordering. Generate the ID in an `itemDefault` function. Do not store identity-sensitive external state under a generated row path. ## Check common mistakes * Use `defaultValues`, not `itemDefault`, to supply initial rows. * Use paths relative to the current array item in `children`. * Use an `itemDefault` function when a new item needs a unique ID. * Use an array node for repeatable rows, not for one multiselect value. # Production recipes Use this page after the first generated form works. Choose the recipe that matches the problem in your application. | Problem | Use | | --- | --- | | Add product UI around generated fields | One binding with `kit.Form` and `kit.Fields` | | Replace the document being edited | A React `key` or `form.api.reset(nextInput)` | | Discard edits | A `type="reset"` button | | Save a new baseline without replacing later edits | `form.api.resetDefaultValues(input)` | | Keep dependent values atomic | `middleware` and `form.update(recipe)` | | Apply a raw RHF update to multiple fields | `form.api.setValues(values)` | | Observe values without a form render subscription | `form.api.subscribe` | | Validate one wizard step | `form.api.trigger` and `form.api.setFocus` | | Build a complete product workflow | [`useFormWorkflow`](/workflows) as application code | | Save parsed data | The `value` argument in `onSubmit` | | Show a request failure | Application state with `role="alert"` | | Show a server field issue | `form.api.setError` | | Load one remote option list | Async field `options` | | Coordinate request state with form UI | Form context and `fromResource` | | Lock the complete form | The `disabled` or `readOnly` option | | Add a design-system control | `defineControl` and the full `ControlProps` contract | React Hook Form owns editable form state. The application owns requests, caches, persistence, navigation, permissions, and product workflow state. ## Compose generated and custom UI Create one form binding for one form. Put generated fields, application actions, RHF fields, and watched state inside the same `kit.Form`. ```tsx // [!include ~/snippets/production-recipes.tsx:composition] ``` Use RHF `Controller`, `useController`, or `register` for custom fields. Use `useWatch` and `useFormState` for derived UI. The unchanged methods on `form.api`, such as `setValue`, remain available for application actions. Do not copy a form value into React state. Two editable copies can become inconsistent. Do not create a second form binding for another panel that edits the same document. Use `kit.AutoForm` when the error summary and generated fields can render first. It renders custom children after the generated fields. Use `kit.Form` with `kit.Fields` when custom content needs another position or no error summary. ## Load an edit-form baseline Wait for the document before you mount the component that calls `kit.useForm`. Use the document ID as a React `key` when navigation can replace the document. ```tsx // [!include ~/snippets/production-recipes.tsx:edit-baseline] ``` The form definition and synchronous `defaultValues` are fixed for one component mount. Changing either prop does not replace current values. Do not use this conditional update as a document navigation workflow. Remount the editor for a different document. Call `form.api.reset(nextInput)` only when the application intentionally replaces the current work. ## Reset or discard changes A reset without arguments restores the current baseline. A reset with a value installs that schema input as the next baseline. ```tsx // [!include ~/snippets/production-recipes.tsx:reset-baseline] ``` `kit.Form` handles the native reset event. It prevents the browser from resetting controls independently and calls `form.api.reset()`. Use these operations for different results: | Operation | Result | | --- | --- | | `form.api.reset()` | Restore the current baseline and clear form metadata. | | `form.api.reset(nextInput)` | Install `nextInput` as the clean baseline. | | Change `defaultValues` | No effect until the hook remounts. | | Change the editor `key` | Create a new form for another document. | ## Update the saved baseline without replacing new edits Requires React Hook Form 7.77.0 or newer. Use `resetDefaultValues` after the server saves the submitted input. This operation changes the clean baseline. It does not replace the current field values. ```tsx // [!include ~/snippets/production-recipes.tsx:saved-baseline] ``` The preview waits before it completes the save operation. Submit the form, then change the name. The new name stays in the field and the form stays dirty. The saved-baseline preview runs only in a browser. It updates the clean baseline without replacing edits made during the save operation. Source: [docs-site/src/snippets/production-recipes.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/production-recipes.tsx) Use the submitted `input` when the server accepts that exact input. Use the server's canonical schema input when the server returns different editable values. ## Keep dependent values atomic Configure value middleware when one generated change must update dependent fields before observers see the result. Call `form.update` when an application action needs the same managed boundary. For one form-local rule, use `beforeUpdate` to adjust or cancel the proposal and `afterUpdate` to observe its final committed transaction. Use middleware when independent policies must compose in an explicit order. ```tsx // [!include ~/snippets/api-reference.tsx:update-hooks] ``` See [Value middleware](/middleware) for complete setup, live previews, cancellation, ordering, array constraints, and validation timing. ```tsx // [!include ~/snippets/api-reference.tsx:value-middleware] ``` Middleware receives Immer patches and a `nextValues` value derived from them. Forward the original patches, append dependent patches, or return without `next` to cancel the complete proposal. Code after synchronous `next` reads the committed final value. Generated controls and `form.update` make one RHF value publication. Generated array actions retain native `useFieldArray` row identity and promise one final React render, not one raw RHF publication. Direct `form.api` operations bypass middleware, so keep invariant-preserving application actions on `form.update`. Only the proposed generated array action can change generated array length or order. Do not change another generated array's structure in the same transaction. Use that array's generated action to keep row IDs synchronized. Use raw `useFieldArray` methods only when bypassing middleware is intentional. Do not call `next` after `await`, call it twice, or start another managed update inside an active transaction. Use an RHF subscription when you only need to observe values; subscriptions run after commit and cannot make dependent changes atomic. ## Apply one update to multiple fields Form, Please requires React Hook Form 7.76.1 or newer. Use raw `setValues` when one application action changes multiple fields but must deliberately bypass Form, Please middleware. One call updates the fields and applies the same dirty, touch, and validation options to each value. ```tsx // [!include ~/snippets/production-recipes.tsx:atomic-values] ``` The preview applies one profile template to three fields. The atomic-values preview runs only in a browser. One action updates three profile fields with the same form-state options. Source: [docs-site/src/snippets/production-recipes.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/production-recipes.tsx) Pass only the fields that the action owns. Do not use `setValues` to install a new document baseline. Use `reset(nextInput)` for that operation. Use `form.update` instead when application middleware must enforce invariants. ## Save a draft with a subscription Requires React Hook Form 7.76.1 or newer, which is the package peer minimum. Use `subscribe` when an external process needs current form values. The subscription does not rerender the form when values change. This preview updates application state only to show the saved draft. ```tsx // [!include ~/snippets/production-recipes.tsx:draft-subscription] ``` The preview waits 400 ms after the last change, then stores the current draft. The draft-subscription preview runs only in a browser. It saves a value snapshot 400 ms after the last form change. Source: [docs-site/src/snippets/production-recipes.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/production-recipes.tsx) Return the unsubscribe function from the effect cleanup. Do not call form mutation methods from the subscription callback. ## Validate one wizard step Requires React Hook Form 7.76.1 or newer, which is the package peer minimum. Use `trigger` with the paths for the current step. Use `setFocus` when the step contains an invalid field. Hidden fields keep their values when the next step renders. ```tsx // [!include ~/snippets/production-recipes.tsx:step-validation] ``` The preview marks the current fields as touched before validation. This action makes their validation messages visible without a form submission. The step-validation preview runs only in a browser. It validates the visible step, focuses its first invalid field, and preserves values between steps. Source: [docs-site/src/snippets/production-recipes.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/production-recipes.tsx) Keep the complete form rules in the Standard Schema. The path list controls step navigation. It does not create a second validation contract. For typed paths, conditional screens, progress, review, and first-invalid navigation, follow the complete [Product workflows tutorial](/workflows). ## Keep async submission in the application Return the request promise from `onSubmit`. React Hook Form keeps `isSubmitting` true until that promise settles. `kit.Submit` disables its button while validation or submission is in progress. ```tsx // [!include ~/snippets/production-recipes.tsx:async-submit] ``` Clear an old request error before each attempt. Render the new failure from application state. Reset the form only after the server returns a successful, canonical input. A transport failure is not a schema issue. Keep it out of the form error summary. If the server identifies invalid field values, adapt that response to the schema or RHF `setError`. The visible `kit.Submit` prevents normal duplicate clicks. If application code calls `form.api.handleSubmit(onValid, onInvalid)`, that code must also prevent concurrent calls. Raw `handleSubmit` does not invoke the configured "Form, Please" wrapper. ## Apply server field errors Use `form.api.setError` when the server identifies a specific editable field. Keep request and form-level failures in application state. First, define and validate the server response: ```tsx // [!include ~/snippets/production-recipes.tsx:server-response] ``` Then apply only the validated field paths: ```tsx // [!include ~/snippets/production-recipes.tsx:server-field-errors] ``` Validate the server response before you apply it. Map only known server paths to schema input paths. A generated control displays the error after the submit attempt. `shouldFocus` moves focus to the first server field error. Clear old server errors before the next request. Do not convert network, authorization, or service failures into field errors. ## Send schema output and reset with schema input Standard Schema can transform data. In that case, editable input and submitted output are different contracts. ```tsx // [!include ~/snippets/production-recipes.tsx:parsed-output] ``` `onSubmit` receives these values: * `input` is the current `FormInput`. * `value` is the parsed `FormOutput`. * `form` is the current form binding. * `submitter` is the native submit control snapshot or `null`. Send `value` when the server expects parsed output. Pass schema input to `form.api.reset`. If the server returns only output, convert it to editable input at the application boundary. Keep schema parsing deterministic. Form, Please parses once for validation and returns the transformed output from that parse. The schema must still be free of side effects because React Hook Form can validate it again after later user actions. ## Choose the transport format "Form, Please" does not serialize the submit value. Send JSON when the server accepts the parsed object: ```ts // [!include ~/snippets/production-recipes.tsx:json-request] ``` Build `FormData` explicitly when the request includes a file: ```ts // [!include ~/snippets/production-recipes.tsx:multipart-body] ``` Do not set the `content-type` header when `fetch` sends `FormData`. The browser adds the multipart boundary. Handle non-success responses with the async submission pattern above. ## Pass request state through context Load remote data before or beside the form. Convert the current request result to `ResourceState`, then pass it through form context. `fromResource` converts that state into synchronous UI values. ```tsx // [!include ~/snippets/production-recipes.tsx:context-resource] ``` The example derives select options, description text, and disabled state from one department resource. Each resolver receives the complete schema input and the current context. `fromResource` does not fetch, cache, retry, or cancel a request. The application or request library owns that work. A resolver must return immediately and must not update form state. See [Resource state](/resources) for pending, success, error, refresh, and TanStack Query examples. ## Choose disabled or read-only mode Set form-wide interaction state when permissions or workflow state apply to all generated fields. ```tsx // [!include ~/snippets/production-recipes.tsx:form-modes] ``` `disabled` disables generated controls and array actions. It also prevents `kit.Form` from submitting. `readOnly` requests read-only behavior from each control, but it does not disable the submit button. Omit the submit action outside edit mode. A custom control must implement both states according to its platform semantics. ## Preserve the accessibility contract Generated field slots connect labels, descriptions, and issue output to a control. A custom control must attach the supplied properties to its interactive element. ```tsx // [!include ~/snippets/production-recipes.tsx:accessible-control] ``` A custom control must: 1. Attach `input.id`, `input.name`, and `input.ref`. 2. Attach `input["aria-describedby"]` and expose `meta.invalid`. 3. Call `setValue` after a valid control change. 4. Call `blur` from its blur event. 5. Honor `disabled`, `readOnly`, and `required`. Custom slots must preserve semantic groups, labels, issue output, and usable buttons. Test keyboard input, focus, invalid state, and read-only behavior for each reusable custom control. ## Test the public experience Test the form through public package imports and visible behavior: 1. Render the component that calls `kit.useForm`. 2. Find controls by their labels and roles. 3. Enter schema input and submit with the visible button. 4. Assert issues, focus, parsed output, and pending UI. 5. Test request and resource branches as application behavior. Do not test private definition normalization or focus maps. Test a reusable custom control once against `ControlProps`. Test each product form as a user operates it. Read the [API reference](/api) for individual contracts. Read [Controls and slots](/form-kits) for design-system integration. # Product workflows Use this tutorial when one form spans multiple product screens. You will keep one "Form, Please" binding and add application-owned workflow state around it. At the end, the workflow will provide: * typed screen paths and conditional screens; * current-screen validation and progress; * navigation to the first invalid screen; * review and confirmation screens; * draft-safe navigation guards; * validated server issue mapping; * draft, publish, and save-and-close actions. ## Keep each state with its owner Use the Standard Schema for submitted domain input. Use React state for the current workflow screen. Pass the screen through form context when a definition must change field visibility. | State | Owner | | --- | --- | | Editable domain input | React Hook Form and the Standard Schema | | Current screen and navigation history | Application state or the router | | Screen visibility | Form context and definition resolvers | | Draft storage | `form-please/persistence` and the application adapter | | Requests and confirmation receipts | Application request state | Put a workflow value in the schema only when the server must store that value as domain data. For example, an approval `status` can be domain data. A current wizard `screen` is normally not domain data. The [makerspace example](/examples/makerspace-launch) follows this boundary. It stores `screen` in React state and supplies it through form context. ## 1. Add a typed workflow controller Copy this complete controller and example into application code: ```tsx twoslash // [!include ~/snippets/product-workflow.tsx] ``` `useFormWorkflow` accepts four inputs: | Input | Purpose | | --- | --- | | `form` | Uses the existing "Form, Please" binding and RHF API. | | `screen` | Reads the current application-owned screen. | | `setScreen` | Changes the external screen state. | | `steps` | Defines labels, typed paths, and optional conditions. | Each `paths` value must be a `FieldPath>`. TypeScript rejects a misspelled path before the application runs. The `when` function reads the current input and includes or removes a conditional screen. `when` changes navigation only. Express the same conditional requirement in the Standard Schema so a hidden screen cannot block final submission. If a condition can hide the active screen, let the application choose the next external `screen`; that fallback is a product-navigation decision. The hook returns these operations and values: | Result | Behavior | | --- | --- | | `next()` | Touches and validates the current screen before navigation. | | `back()` | Opens the previous visible screen without validation. | | `validateCurrent()` | Validates the current screen without navigation on success. | | `validateAllAndFocusFirstInvalid()` | Validates all visible paths, opens the first invalid screen, and focuses its first invalid field. | | `visibleSteps` | Contains the steps that satisfy their current conditions. | | `progress` | Contains `current`, `total`, and integer `percent` values. | The controller uses `form.api.trigger`. It does not create another validator. Keep conditional rules in the Standard Schema, and give navigable issues a field path. A form-level issue without a path cannot identify a screen. ### Try the workflow Clear the organization checkbox to remove its conditional screen and recalculate progress. On the details screen, select **Clear identity name** and then **Review**. The controller opens the identity screen and focuses **Name**. The product-workflow preview runs only in a browser. It validates each visible screen, skips conditional screens, shows progress, and opens the first screen with invalid input. Source: [docs-site/src/snippets/product-workflow.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/product-workflow.tsx) ## 2. Reuse the binding for review Review shows the current editable input. It is not a saved confirmation. Keep the same binding and definition, set `readOnly`, and provide an **Edit** action. After a successful request, replace review with a confirmation that uses the server receipt. The receipt can contain canonical values, generated IDs, and other authoritative data. ```tsx twoslash // [!include ~/snippets/workflow-review.tsx] ``` Do not create a second binding for review. Two bindings can create two editable copies of the same document. ## 3. Flush a draft before router navigation Install React Router when the application does not already use it: ```bash npm install react-router ``` Use `useBlocker` in a React Router data or framework router. When the user chooses **Save draft and leave**, wait for `persistence.flush()` before you call `proceed()`. Pass `useFormState({ control: form.api.control }).isDirty` as the guard's `dirty` value. ```tsx twoslash // [!include ~/snippets/workflow-router-guard.tsx] ``` Use the application's accessible modal component for this dialog. It must trap focus while open, support Escape, and restore focus after **Stay**. If `flush()` fails, keep navigation blocked and show the storage error. The **Leave without saving** action deliberately skips `flush()`. `useBlocker` handles SPA navigation. `beforeunload` covers reloads, tab closes, and cross-origin navigation with a browser warning. A `beforeunload` callback cannot wait for an asynchronous flush. Autosave before this event; do not claim an unload save guarantee. ## 4. Map server issues at the request boundary Do not cast an arbitrary server string to `FieldPath`. Validate the response, then map each remote path to one known schema input path. ```tsx twoslash // [!include ~/snippets/workflow-server-issues.tsx] ``` The `satisfies` checks make both maps exhaustive. If the server adds a path or the form renames a field, TypeScript requires a mapping update. Apply all errors before focus navigation. Then open the screen for the first mapped path and focus after that screen renders. Keep network, authorization, and form-level failures in application request state. ## 5. Separate draft and validated actions A draft can contain incomplete or invalid input. Use `type="button"` for **Save draft** and call `persistence.flush()` directly. This action does not submit the form and does not run complete schema validation. Use `kit.Submit` for **Publish** and **Save and close**. Both actions validate the complete Standard Schema before `onSubmit` runs. ```tsx twoslash // [!include ~/snippets/workflow-submit-actions.tsx] ``` The helper constrains application intent values even though native button `value` is a string. The handler treats only a `null` implicit submit as **Publish** and rejects unexpected submitter names or values. Choose a different explicit default if the product requires it. ## Understand `submitter` `FormSubmitDetails` includes this property: ```ts twoslash import type { FormSubmitDetails, StandardSchema } from "form-please" type Input = { title: string } declare const schema: StandardSchema type Submitter = FormSubmitDetails["submitter"] // ^? ``` The value is `Readonly<{ name: string; value: string }> | null`. The submit sequence is: 1. `kit.Form` prevents the native submit. 2. It captures the editable input and native submit control. 3. The Standard Schema validates the captured input. 4. A successful `onSubmit` receives `value`, `input`, `form`, and `submitter`. The snapshot has these guarantees: * It is captured before validation starts, including asynchronous validation. * It contains only the submit control `name` and `value` strings. * It is frozen and does not retain a live DOM element. * It is `null` when the native event has no submit control, such as some Enter submissions or a manually dispatched submit event. * It is separate from `value`, `input`, native `FormData`, and request data. Changing or removing the button while validation is pending does not change the snapshot. Direct `form.api.handleSubmit` calls remain raw React Hook Form calls and do not invoke the "Form, Please" `onSubmit` wrapper. Use `submitter` only to select a validated application action. Validate the action again at the server boundary. Do not use a button value as authorization. ## Production checklist Before release, verify these behaviors: 1. Change every condition and confirm that progress uses visible screens only. 2. Put an error on each screen and confirm first-invalid navigation and focus. 3. Confirm that review uses current input and confirmation uses a server receipt. 4. Make draft storage fail and confirm that guarded navigation remains blocked. 5. Return every supported server issue path and confirm its mapping. 6. Submit each named action and an implicit submit. 7. Test keyboard navigation, focus order, and the guard dialog. # Resource state Use resource state when remote data changes labels, descriptions, permissions, or interaction state in a form. For a simple selectable list, prefer an async field `options` function. Use resource state when the application must expose pending or error state, retain previous data, cache, retry, search, or coordinate several UI properties. | Required result | Helper | | --- | --- | | Derive a value immediately | `matchResource` | | Define a synchronous UI resolver | `fromResource` | | Load or refresh remote data | Application code or a request library | | Load one selectable list without request UI | Async field `options` | ## Define the resource states `ResourceState` is a readonly tagged union with three states: ```ts type ResourceState = | { readonly status: "pending" } | { readonly status: "success"; readonly value: Value } | { readonly status: "error"; readonly error: Error } ``` Set the `Error` type when a branch must read a known error property. The default error type is `unknown`. ```ts // [!include ~/snippets/resources-guide.tsx:create-states] ``` The `status` property selects the branch. A successful state must contain `value`. An error state must contain `error`. ## Match a standalone resource Use `matchResource` when you already have a resource and need one result now. For example, create status text outside a form definition: ```ts // [!include ~/snippets/resources-guide.tsx:match-resource] ``` All three callbacks are required. Each callback receives the narrowed state: | Callback | Available state data | | --- | --- | | `pending` | `status` | | `success` | `status`, `value` | | `error` | `status`, `error` | The result type is the union of the three callback results. Keep the results compatible when a caller requires one specific type. ## Build a UI resolver Use `fromResource` for a property in a form definition. Its first argument selects a resource from the current form values or context. Its second argument maps each state to the required property value. ```ts // [!include ~/snippets/resources-guide.tsx:resource-resolvers] ``` Each branch receives these arguments in this order: 1. The narrowed resource state. 2. The complete readonly form input. 3. Resolver details, which contain the readonly form context. Use only the arguments that the branch needs. In the example, the success description reads the selected plan. The options function reads saved options from context during pending and error states. `fromResource` returns a normal synchronous UI resolver. The selector and all branches must return synchronously. Load data before or beside the form. Do not pass an `async` function to `fromResource`; field `options` has its own async contract. ## Pass the resource through context Create a contextual kit. Then pass the current context to `kit.useForm`. ```tsx // [!include ~/snippets/resources-guide.tsx:context-form] ``` The example uses one resource for three properties: * `options` keeps saved options until the current request succeeds. * `description` reports the current state. * `disabled` prevents selection without usable remote data. Form context is runtime input. When the application supplies a new context value, the generated UI resolves again with the current resource state. ## Adapt TanStack Query locally Keep the query result in application code. Convert its states before you pass the result through form context. This adapter preserves background refresh state without adding a TanStack Query dependency to "Form, Please": ```ts twoslash // [!include ~/snippets/query-to-resource.ts] ``` Use the adapter at the boundary between the query and the form: ```tsx // [!include ~/snippets/resources-guide.tsx:query-context] ``` The adapter maps query states as follows: | Query result | Resource result | | --- | --- | | Initial load | `pending` | | Loaded data | `success` | | Failed background refetch with old data | `success` with `refresh.error` | | Failed request without data | `error` | The background refetch error stays in the success branch because usable data still exists. This lets the UI keep saved options and report the refresh error. The top-level error branch does not contain previous data. `QueryResourceState` adds `fetchStatus` and `refresh` properties to the basic three-state shape. `matchResource` and `fromResource` preserve these extra properties when they narrow a branch. ## Handle invalid states and branch errors TypeScript prevents unsupported status values in typed application code. At runtime, both helpers throw a `TypeError` for an unsupported status. They do not catch errors that a selector or branch callback throws. Keep resource objects inside the supported union. Handle request failures when you create the resource. Handle UI conversion failures where you call the helper. ## Preserve request ownership * The request library owns requests, retries, cancellation, and caching. * The application converts request results to a resource state. * Form context carries the current resource state. * `fromResource` converts that state into a synchronous UI property. See [Creative studio policies](/examples/studio-policies) for a live form with saved options and background refresh state. # Value middleware Value middleware intercepts managed value proposals before Form, Please commits them to React Hook Form. Use it to derive dependent values, cancel a proposal, or run ordered work after a commit. Middleware is form-local. It does not create another form store. React Hook Form remains the owner of editable values and form state. | Need | Use | | --- | --- | | Adjust or cancel one form-local proposal | `beforeUpdate` | | Observe one final transaction with its source and patches | `afterUpdate` | | Compose independent value policies | Value middleware | | Run an application action through the same rules | `form.update(recipe)` | | Add grouped undo and redo | [Managed value history](/history) | | Observe values after a commit | An RHF subscription | | Make an independent raw RHF change | `form.api` | | Validate or transform submitted output | The Standard Schema | ## Use one managed update hook `beforeUpdate` and `afterUpdate` provide one application callback on either side of the middleware chain. The first callback can mutate the proposed Immer draft or return `false`; the second receives the final committed transaction. ```tsx // [!include ~/snippets/api-reference.tsx:update-hooks] ``` The order is `beforeUpdate`, middleware before `next`, commit, middleware after `next`, and `afterUpdate`. Both hooks are synchronous. `afterUpdate` still runs when middleware throws after a commit, because the committed change remains observable. If both fail after commit, dispatch throws an `AggregateError`. The callbacks use their latest React render versions. Their transactions are readonly views rather than archival snapshots. Copy retained audit or save data. Use middleware when several rules must compose in a declared order. Generated array constraints still apply. Return `false` to cancel the proposed structural action; do not rewrite its length or order through the draft. ## Check editing in a larger form Use this larger form to check editing in your browser. It renders 18 text inputs and sends each edit through one pass-through middleware. Edit fields quickly. Move focus between sections while you type. The complex editing preview runs only in a browser. It renders 18 text inputs and sends each edit through middleware. Source: [docs-site/src/snippets/middleware-guide.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/middleware-guide.tsx) This preview is a manual check, not a repeatable benchmark. Browser extensions, computer load, and development tools can change the result. Use `npm run bench:middleware` to measure the coordinator without browser rendering. The automated browser test also checks that rapid edits do not lose input. ## Derive a value in the same commit This middleware calculates an order total. It appends one Immer patch to the patches for the source change. ```tsx // [!include ~/snippets/middleware-guide.tsx:derived-value] ``` Pass the ordered middleware list to `kit.useForm`. Use `form.update` when an application action must enter the same managed boundary. ```tsx // [!include ~/snippets/middleware-guide.tsx:derived-value-form] ``` Change the quantity or unit price. The read-only total changes in the same managed commit. The button changes both source fields with one Immer recipe. The derived-total preview runs only in a browser. Middleware updates a read-only total in the same managed commit as its source values. Source: [docs-site/src/snippets/middleware-guide.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/middleware-guide.tsx) The managed change has this sequence: 1. Form, Please creates patches for the proposed source change. 2. The middleware calculates the total from `transaction.nextValues`. 3. The middleware calls `next` with the source patches and the total patch. 4. React Hook Form receives the final affected values before `next` returns. The transaction received by this middleware contains the value before its new total patch. The next middleware receives a new transaction with the recalculated `nextValues`. ## Cancel a managed change Return without calling `next` to cancel the complete proposal. No source patch or dependent patch commits. ```tsx // [!include ~/snippets/middleware-guide.tsx:cancellation] ``` The first button uses `form.update`, so the 30% limit applies. The second button uses raw `form.api.setValue`, so it bypasses the limit. The cancellation preview runs only in a browser. It compares a cancelled managed change with a raw React Hook Form update that bypasses middleware. Source: [docs-site/src/snippets/middleware-guide.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/middleware-guide.tsx) Use raw RHF operations only when bypassing middleware is intentional. A direct operation can violate an invariant that middleware enforces for managed changes. ## Know which changes enter middleware Middleware handles these entry points: | Entry point | `transaction.source.type` | | --- | --- | | Generated control change | `control` | | Generated array add, remove, or move | `array` | | `form.update(recipe)` | `update` | | History undo, redo, seek, or import | `history` | | Persistence restore | `persistence` | These operations bypass middleware and managed update hooks: * initial `defaultValues`; * `form.api.reset` and native form reset; * direct `form.api` mutations, including `setValue` and `setValues`; * application-owned `useFieldArray` operations. Supply consistent derived values in `defaultValues` and every reset input. Middleware does not calculate dependent values for these entry points. Active persistence can observe their resulting RHF values for autosave, but they do not become middleware transactions. `form.update` accepts a synchronous Immer recipe. Mutate the draft or return a replacement value. Do not do both. A recipe that produces no patches does not create a transaction. ## Read a transaction Each middleware receives one `ValueTransaction`. | Field | Meaning | | --- | --- | | `previousValues` | Values before the managed proposal. | | `nextValues` | Values produced by the current patches. | | `patches` | Authoritative Immer `add`, `remove`, and `replace` operations. | | `source` | The generated control, generated array action, `form.update`, or optional feature restore. | | `context` | Current deeply readonly runtime context for the form binding. | Control sources contain a typed `path`. Array sources contain a typed `path`, an action, and the affected index. Move sources contain `fromIndex` and `toIndex`. Optional history restores contain an `undo`, `redo`, `seek`, or `import` action. Persistence restore contains a `restore` action. Patch paths are segment arrays such as `["items", 0, "price"]`. They are not RHF dot paths such as `items.0.price`. The value and context fields are deeply readonly TypeScript views. They are not frozen or cloned as archival snapshots. Read them synchronously. Copy data that must remain an independent snapshot. Do not mutate the transaction, its patches, its values, or its context. Supply a new patch array to `next` when middleware changes a proposal. ## Use the middleware API The first function receives one `FormMiddlewareApi` when the form creates its fixed middleware chain. | Method | Result | | --- | --- | | `api.getValues()` | Read the current RHF values as a deeply readonly TypeScript view. | | `api.update(recipe)` | Start a new managed update when no transaction is active. | Call `api.getValues()` after synchronous `next` to read the committed result from downstream middleware. The returned value is not an independent snapshot. Do not call `api.update` while middleware configures or while a transaction is active. An asynchronous handler can call it after awaited post-commit work. That call starts a separate transaction. ## Forward patches once Middleware has the shape `api => next => transaction`. Call `next(patches)` synchronously at most once. You can forward the original patches, append patches, or replace the patch list. Form, Please reapplies the supplied patches to `previousValues`. This operation creates the transaction for the next middleware. Middleware runs in list order. With `[deriveTotal, auditOrder]`, `deriveTotal` runs first. `auditOrder` receives the derived total. Code after `next` runs in reverse order and sees committed values. The chain makes one ordered pass. It does not rerun middleware when a later middleware adds a patch. It does not calculate a dependency fixed point. Any middleware can replace the value returned by `next`. Therefore, `FormMiddlewareNext` and `form.update` return `unknown`. Do not use that return value as a commit result unless the complete chain defines that convention. ## Run asynchronous work after the commit Call `next` before the first `await`. An asynchronous handler can return a promise after the synchronous commit. ```tsx // [!include ~/snippets/middleware-guide.tsx:async-after-next] ``` In this order, `auditCommittedOrder` receives the total patch from `keepOrderTotalCurrent`. The value commit is complete before `saveOrderAudit` starts. Middleware completion does not mean validation or the asynchronous work is complete. Observe RHF validation state when later code requires valid values. ## Handle generated arrays safely Generated add, remove, and move actions enter middleware. Middleware can cancel the action or add dependent value patches. The source action still uses RHF `useFieldArray` so the row IDs remain stable. Do not change the source array length or order beyond its proposed action. Do not change another generated array's structure in the same transaction. Use that array's generated action for its structural change. Do not change generated array length or order from a generated control or `form.update`. RHF `setValues` cannot synchronize the private row IDs that `useFieldArray` owns. Generated array actions promise one final React render with dependent values. They do not promise one raw RHF publication. A raw RHF subscriber can observe the native array operation before the dependent value commit. ## Understand form state and validation The value commit completes before `next` returns. Validation can finish later. Managed validation follows `mode` and `reValidateMode` through one RHF `trigger` call. This trigger does not preserve `delayError` timing. RHF calculates `isDirty` from the complete current value and default values. `dirtyFields` is guaranteed only for mounted patched paths. A control becomes touched only after a real blur event. ## Avoid invalid pipeline operations | Operation | Result | | --- | --- | | Call `next` after `await` | Throws before stale patches can commit. | | Call `next` twice | Throws; an earlier commit is not rolled back. | | Call `api.update` during a transaction | Throws because nested managed updates are not allowed. | | Remove a top-level key | Throws because RHF shallow-merges root values. | | Throw before `next` | Prevents the commit. | | Throw after `next` | Propagates the error without rolling back the commit. | Assign `undefined` instead of removing a top-level key when the schema permits it. Nested removal is supported because Form, Please commits the complete affected root. The middleware list is fixed for the `useForm` hook lifetime. Change the React `key` to create a form with another list. The current `transaction.context` still follows the latest form context. See the [API reference](/api#managed-value-middleware) for the exported types. # Form persistence Import `createPersistenceMiddleware` from `form-please/persistence` when a form needs a durable editable draft. React Hook Form still owns the live values and form state. Persistence stores only the complete schema input. ## Configure and restore Create one feature, add that exact reference to `middleware`, and pass the form and feature to `usePersistence`. The hook subscribes to persistence state and starts `restore()` after mount. It loads at most once after a successful or empty result. ```tsx // [!include ~/snippets/persistence-local-storage.tsx:local-storage] ``` The lazy storage getter keeps `localStorage` access out of module evaluation. The hook starts restore only after the client mounts. `restore()` returns one of these results: * `"applied"`: the stored input became the live input; * `"empty"`: no stored draft exists; * `"cancelled"`: middleware did not commit the restore; * `"transformed"`: middleware changed the restored input; * `"conflict"`: live input changed while the adapter was loading. An applied restore uses the managed update pipeline with a `{ type: "persistence", action: "restore" }` source. It preserves the original `defaultValues`, recalculates dirty state, and clears errors, touched state, and submission metadata. It does not run validation. A draft may contain temporary invalid input. ## Resolve a restore conflict Persistence never overwrites a local edit made while `load()` is pending. The handle enters the `conflict` phase and returns `"conflict"`. Call `start()` to keep the current form and save it. Remount the form if the user must discard the local input and try the load again. Use the lower-level `feature.handle(form)` and root `useSnapshot` helper when the application must call `start()` without an initial load. A normal start does not write the unchanged initial values. A start after a conflict or restore failure immediately saves the current input. ## Observe and control saves After restore or start, persistence observes all RHF value publications. This includes generated controls, `form.update`, and direct `form.api` changes. Autosave uses a trailing 500 ms delay by default. Set `saveDelay` to another finite non-negative number. The hook result includes a reactive `snapshot` and these handle operations: * `flush()` cancels the delay and waits for the latest active input to save; * `clear()` removes stored data without changing live input; * `getSnapshot()` returns the restore phase and save status; * `subscribe()` implements the external-store contract for `useSnapshot`. The hook handles the restore Promise rejection because the same error remains available through the `failed` snapshot and the feature's `onError` callback. Call the returned `restore()` operation to retry explicitly after a failure. Writes are sequential and coalesced. If input changes during a save, the latest input is saved afterward. Clearing an active draft suppresses an immediate rewrite. The next edit creates the draft again. Storage errors never roll back form values. A failed save reports `save.status === "failed"` with `operation: "save"` or `"clear"`. The next edit or `flush()` retries. A restore failure uses the top-level `failed` phase and can be retried with `restore()`. The optional `onError` callback receives the error and a `PersistenceErrorDetails` object. Import that type from `form-please/persistence` when the observer is declared separately; its `operation` is `"restore"`, `"save"`, or `"clear"`. ## Own the adapter An adapter is a keyed asynchronous transport: ```ts type FormPersistenceAdapter = { load(key: string): Promise save(key: string, value: JsonValue): Promise remove(key: string): Promise } ``` Form Please owns the JSON-safe envelope. The application owns storage, authentication, authorization, request cancellation, and retention. ### Query string with nuqs Use `replace` history and shallow URL updates so autosave does not create a browser history entry or navigation for each edit. ```ts twoslash // [!include ~/snippets/persistence-nuqs.ts] ``` The live [Query string persistence example](/examples/persistence) shows the adapter with `useQueryState` and `NuqsAdapter`. ### TanStack Query TanStack Query can coordinate a remote draft request and its client cache. The request functions remain application-owned. ```ts // [!include ~/snippets/persistence-tanstack-query.ts:tanstack-query] ``` ## Version and encode drafts Each stored envelope contains the persistence protocol version, the application `version`, and the encoded payload. Increase `version` when the editable input shape changes. Supply `migrate(value, fromVersion, toVersion)` to convert the decoded old value. A successful migration is immediately rewritten in the current envelope. Plain values support `null`, booleans, strings, finite numbers, `undefined`, arrays, and plain objects. Unsupported values fail with their input path. Add `createDateCodec()` for `Date`. Add another explicit `PersistenceCodec` for any other opaque value that must cross the storage boundary. Do not treat a persisted draft as trusted input. Standard Schema still validates on submit, and a server must validate every submitted value again. ## Boundaries One form accepts one persistence feature. The feature does not synchronize multiple tabs or forms, save on page unload, retain RHF metadata, or persist managed history. The adapter can add cross-client coordination when the product requires it. # Managed value history Import `createHistoryMiddleware` from `form-please/history` when users need to navigate earlier editable values. History is optional and does not create another live form store. React Hook Form remains the owner of current values and form state. ## Configure one history feature Create the feature outside the component, put that exact reference in the form's fixed middleware list, and pass both values to `useHistory`: ```tsx // [!include ~/snippets/history-guide.tsx:setup] ``` The returned handle includes a reactive `snapshot` with `canUndo`, `canRedo`, `index`, and `length`. It also keeps the underlying `getSnapshot()` and `subscribe()` external-store operations. One feature can serve several forms. Each form can configure only one history feature. `useHistory(form, feature)` and the lower-level `feature.handle(form)` reject a form that does not use that exact feature. ## Know what history records History records successful managed value updates from generated controls, generated array actions, and `form.update`. It retains complete independent schema-input snapshots rather than patches or events. Initial values, native reset, direct `form.api` mutations, and application-owned `useFieldArray` operations bypass history. When raw values diverge, the next managed update or history operation makes those current values a new non-undoable boundary and discards the older branches. History never retains errors, touched state, validation, submission, focus, context, or the default-value baseline. Restore keeps the current RHF metadata, recalculates dirty state against the original defaults, and may regenerate private field-array row IDs. ## Group and retain positions Consecutive control edits to one path share a group for 750 milliseconds by default. Another source, another path, or an expired window starts a group. Set `groupWindow: 0` to make every managed update separately undoable. `limit` defaults to 100 retained groups. Set another non-negative integer or `Infinity`. A limit of zero retains only the current position. ## Navigate through middleware `undo()`, `redo()`, `seek(index)`, and `import(value)` return promises. Their managed transaction source has `type: "history"` and an `undo`, `redo`, `seek`, or `import` action. `beforeUpdate` and middleware can cancel or transform the target, and `afterUpdate` observes the final commit. Operations report `"applied"`, `"unavailable"`, `"cancelled"`, or `"transformed"`. A transformed restore becomes a new group and removes its redo branch. A post-commit failure rejects the operation without rolling back the committed values or history position. ## Export and import an in-memory journal Export from one handle and import into another handle: ```ts // [!include ~/snippets/history-guide.tsx:journal] ``` `HistoryJournal` version 1 contains complete input entries and a numeric current index. Import validates its protocol, index, entry roots, and configured retention limit. It does not require Standard Schema success because editable history can contain temporarily invalid values. The journal is an in-memory navigation artifact, not a JSON persistence format or audit log. `Date` and `RegExp` leaves are detached. Browser-owned values such as `File` and `Blob` preserve their identity. Applications own transport, serialization, storage, and trust boundaries. Try the complete [History workflow](/examples/history). # Examples These examples use the current public "Form, Please" API. Each live form runs in the browser with React Hook Form as its state and validation runtime. ## Design-system integration * [Material UI with Yup](/examples/mui-yup) uses the official Material UI preset and validates the form with Yup through Standard Schema. * [Shadcn with Valibot](/examples/shadcn-valibot) uses an application-owned control and slot adapter. Valibot validates the same definition. ## Custom control integration * [Async multiselect](/examples/async-multiselect) stores selected IDs in one field. TanStack Query loads options, and Floating UI manages the popup. ## Product workflows * [History workflow](/examples/history) adds grouped undo, redo, seek, and in-memory journal transfer to managed value updates. * [Query string persistence](/examples/persistence) restores and autosaves an editable draft through a `nuqs` URL adapter. * [Research grant application](/examples/research-grant) combines conditional applicant fields, registry search, a live preview, and two submission steps. * [Creative studio policies](/examples/studio-policies) passes an asynchronous equipment catalog through form context and adapts it with `fromResource`. * [Makerspace launch wizard](/examples/makerspace-launch) keeps the active stage in form values and combines address lookup, media, pricing, and promotions. * [Learning cohort editor](/examples/learning-cohort) combines remote title suggestions, two movable arrays, media, offers, and conflict feedback. * [Membership ladder](/examples/membership-ladder) combines nested benefit arrays, a workspace action, pause shortcuts, and cross-tier validation. * [Campaign builder](/examples/campaign-builder) supports seven conditional templates with shared audience, schedule, payment, and create-or-edit flows. ## Runtime ownership "Form, Please" resolves definitions and renders controls. React Hook Form owns editable state, array operations, validation, and submission. TanStack Query owns request state in the examples that load or save data. Hidden fields preserve their values. Conditional schemas decide which values are required for the active branch. Successful submission parses the schema a second time and passes transformed output to the submit callback. # History workflow This example adds managed value history to a generated profile form while React Hook Form continues to own live values, validation, dirty state, and arrays. ## Try the workflow 1. Change **Name** several times. 2. Add or edit a project. 3. Select **Undo**, then **Redo**. 4. Select **Export**, make another change, then select **Import**. 5. Select **Clear history** to retain only the current position. The managed value history preview runs only in a browser. It demonstrates grouped editing, undo, redo, seek, journal export and import, and clearing one form's retained positions. Source: [docs-site/src/snippets/history-guide.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/history-guide.tsx) The position output follows retained groups. Operations leave the live form active when no target is available or middleware cancels a restore. ## What this example shows * `createHistoryMiddleware` as an optional `form-please/history` feature. * `useHistory(form, feature)` for the exact configured form. * The hook's reactive `snapshot` for navigation state. * Managed restore through hooks and ordered middleware. * Full input snapshots in `HistoryJournal` version 1. ## Copy the source ```tsx twoslash // [!include ~/snippets/history-guide.tsx] ``` Read [Managed value history](/history) for grouping, retention, raw RHF boundaries, operation results, and journal limits. # Query string persistence This example stores the editable form draft in the URL `draft` parameter. It uses the optional persistence middleware while React Hook Form continues to own live values, validation, defaults, and form metadata. ## Try the workflow 1. Edit **Name** or **Role** and watch the URL after 250 ms. 2. Select **Save now** to flush without waiting. 3. Reload the page to restore the query string draft. 4. Select **Clear saved draft** to remove the parameter without changing the current fields. The persistence preview runs only in a browser. It restores and autosaves the editable form input in the URL query string through nuqs. Source: [docs-site/src/snippets/persistence-basics.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/persistence-basics.tsx) The adapter uses `history: "replace"` and `shallow: true`. Autosave does not add a browser history entry for each edit and does not start a page navigation. ## What this example shows * `createPersistenceMiddleware` as an optional `form-please/persistence` feature; * `usePersistence(form, feature)` for automatic restore after mount; * the hook's reactive `snapshot` for restore and save state; * a `nuqs` string state adapted to the keyed asynchronous transport. ## Copy the source ```tsx twoslash // [!include ~/snippets/persistence-basics.tsx] ``` ```ts twoslash // [!include ~/snippets/persistence-nuqs.ts] ``` Read [Form persistence](/persistence) for conflicts, migrations, codecs, storage failures, and lifecycle boundaries. # Material UI with Yup This example uses `form-please/preset-mui`. Material UI renders the controls and structural slots. Yup validates the form through Standard Schema. ## Install the preset peers ```bash npm install form-please react-hook-form @mui/material @emotion/react @emotion/styled yup ``` Create a kit in application code: ```tsx twoslash import { createMuiFormKit } from "form-please/preset-mui" const kit = createMuiFormKit({ i18n: { chooseFile: "Attach slides" }, }) ``` The application owns `ThemeProvider` and its theme. The live example follows the documentation site's light and dark mode. ## Try the conference proposal Change the session to **Workshop** and set experience below three years to see the cross-field Yup rule. Edit the title or abstract to see Yup transforms in the submitted output. The Material UI conference form runs in a browser. It uses the official preset and validates the proposal directly with Yup through Standard Schema. Source: [docs-site/src/snippets/mui-yup-conference.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/mui-yup-conference.tsx) ## Runtime flow `kit.useForm` receives the definition, default values, and submit callback. `kit.AutoForm` renders the binding. The React Hook Form resolver parses the Yup schema once and returns its transformed output to a successful submit. ## Copy the example ```tsx twoslash // [!include ~/snippets/mui-yup-conference.tsx] ``` Read [Form kits](/form-kits) for the adapter contract. Read the [API reference](/api#createmuiformkit) for the preset factory. # Shadcn with Valibot This example keeps the design-system boundary in the application. A local kit maps "Form, Please" control props and slots to shadcn-style components. Valibot provides the Standard Schema. ## Install the adapter ```bash npx shadcn@latest add r13v/form-please/shadcn-form-kit ``` The registry item installs `form-please` and copies the adapter into your application. Install the schema library used by your form separately. This example uses `valibot`. ```bash npm install valibot ``` The item targets shadcn's Base UI implementation. Review the copied component imports when your project uses another style or customized component props. The adapter remains normal application source, so it can follow local aliases, styles, and signatures. ## Try the workshop proposal Change radio and switch values, move the sliders, search the comboboxes, choose calendar values, and edit the OTP. Submit to run the Valibot schema twice and receive transformed output. The shadcn workshop form runs in a browser. It validates with Valibot and renders application-owned Base UI controls from a local adapter. Source: [docs-site/src/snippets/shadcn-valibot-workshop.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/shadcn-valibot-workshop.tsx) ## Adapter surface The local adapter defines native-shaped controls, Base UI option controls, sliders, date pickers, comboboxes, an OTP control, and structural slots. Each control uses the public `ControlProps` contract. The kit uses the public `createFormKit` and `defineControl` functions. Its radio and combobox option types mark their IDs with `OptionValue`, so Valibot `picklist` fields reject option values outside their inferred unions. ## Copy the example ```tsx twoslash // [!include ~/snippets/shadcn-valibot-workshop.tsx] ``` Read [Form kits](/form-kits) for the control and slot contracts. Review the [registry manifest](https://github.com/r13v/form-please/blob/main/registry.json) and [adapter source](https://github.com/r13v/form-please/blob/main/docs-site/src/components/ui/form-please/shadcn-form-kit.tsx) before adapting them to your project. Append a matching Git release tag to the registry item reference, such as `#vX.Y.Z`, when an installation must be reproducible. # Async multiselect This example builds one searchable field that stores a `string[]` value. TanStack Query loads the options. Floating UI supplies the popup interactions. Use this pattern when the option set is remote or too large to load in full. For a short static list, use a simpler local control without TanStack Query. ## Try the example 1. Open the city list. 2. Search for `m`. 3. Select or remove a city. 4. Select **Save selection** to see the submitted IDs. The list stays open when you change a selection. Repeated searches use the TanStack Query cache. The live async multiselect runs only in a browser. It stores selected IDs in React Hook Form, loads matching options through TanStack Query, and manages the popup with Floating UI. Source: [docs-site/src/snippets/async-multiselect.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/async-multiselect.tsx) ## Understand the ownership Each layer has one responsibility: | Layer | Responsibility | | --- | --- | | React Hook Form | Stores selected IDs and field metadata. | | "Form, Please" | Connects the field definition to the custom control. | | TanStack Query | Loads, cancels, and caches option requests. | | Floating UI | Positions the popup and manages its interactions. | | Custom control | Stores search text, labels, and popup state. | The form value contains IDs, not `{ value, label }` objects. A label change does not change the form value or make the form dirty. ## Install the dependencies This example uses Zod as its Standard Schema library. It uses Heroicons for the four interface icons. ```sh npm install form-please react-hook-form @tanstack/react-query @floating-ui/react @heroicons/react zod ``` ## 1. Model one array field Define one array value in the schema. Supply the initial IDs through `defaultValues`. ```tsx // [!include ~/snippets/async-multiselect.tsx:schema-values] ``` Use a `field` node for this value. Do not use an `array` UI node. An array node renders repeatable rows, but this control updates one `string[]` value. ## 2. Define and register the control The control props separate request settings from display text: ```tsx // [!include ~/snippets/async-multiselect.tsx:option-contract] ``` Register the control in the same kit that defines the form: ```tsx // [!include ~/snippets/async-multiselect.tsx:register-control] ``` The `defineControl` call binds the control to `string[]`. The form definition can use `asyncMultiSelect` only for a compatible field path. ## 3. Load and validate API options Pass the TanStack Query `AbortSignal` to `fetch`. Check the HTTP status and validate the response before you return options: ```ts twoslash // [!include ~/snippets/async-multiselect-request.ts] ``` The request rejects when the server returns an error status or invalid data. The control shows a retry action for the rejected query. The live demo uses a local array and an abortable delay. This data source keeps the documentation site deterministic: ```tsx // [!include ~/snippets/async-multiselect.tsx:demo-query] ``` Replace the demo query with the API request in application code. ## 4. Connect search to TanStack Query The control appends the debounced search text to the supplied query key: ```tsx // [!include ~/snippets/async-multiselect.tsx:query-state] ``` The query starts only while the popup is open. `keepPreviousData` keeps the previous result available while the next request loads. Add every value that changes the response to the base `queryKey`. For example, use `queryKey: ["cities", tenantId, locale]` when the tenant and locale change the option list. The generic async field `options` API loads a complete list. This custom control owns remote search instead, because query text, previous results, retries, and loading UI are part of its interaction. Pass the asynchronous function through the control `props` as `queryFn`. ## 5. Configure the field Add the field to the form definition: ```tsx // [!include ~/snippets/async-multiselect.tsx:field-definition] ``` The control requires these request props: | Prop | Purpose | | --- | --- | | `queryKey` | Identifies the option source and its request context. | | `queryFn` | Returns options for one search and supports cancellation. | These properties control request and label behavior: | Prop | Default | Purpose | | --- | --- | --- | | `initialOptions` | `[]` | Supplies labels for initial selected IDs. | | `debounceMs` | `250` | Delays a query after the search text changes. | | `staleTime` | `30_000` | Sets the option cache freshness period. | These properties control visible text and tag count: | Prop | Default | | --- | --- | | `maxVisibleTags` | `3` | | `placeholder` | `"Choose options"` | | `searchPlaceholder` | `"Search options"` | | `emptyMessage` | `"No options found."` | | `dialogLabel` | `"Options"` | ## Preserve labels for selected IDs A new search can replace the current option result. The control keeps every loaded option in a local label cache: ```tsx // [!include ~/snippets/async-multiselect.tsx:label-cache] ``` For an edit form, load the selected records with the form baseline. Pass those records through `initialOptions`. If a label is not available, the control shows the ID until it loads that option. The cache stores labels for display only. Submission still uses the ID array. ## Provide TanStack Query and submit the IDs Reuse the application `QueryClientProvider` when one already exists. The live example creates one client for its own component: ```tsx // [!include ~/snippets/async-multiselect.tsx:provider-submit] ``` `setValue` replaces the complete array when a selection changes. React Hook Form stores that array as input. The Standard Schema validates it during submit. The submit callback receives the validated `cityIds`. The server must confirm that each submitted ID exists and that the user can select it. ## Handle interaction and failure states The control has these states: | State | Result | | --- | --- | | Closed | The option query is disabled. | | Loading | The popup shows `Loading…` and keeps available prior data. | | Error | The popup shows `Could not load options.` and **Try again**. | | Empty | The popup shows the configured `emptyMessage`. | | Read-only | The user can inspect options but cannot change the search or value. | Floating UI supplies click, dismiss, focus-return, positioning, and list navigation behavior. The control handles ID toggling and keeps the popup open. It also forwards the field ID, description relation, invalid state, disabled state, read-only state, and focus ref from `ControlProps`. ## Copy the control implementation The file below contains the control and the live demo wiring. Its CSS class names have no library styles. Add application styles for those class names, or replace them with your design-system components.
Copy async-multiselect.tsx ```tsx twoslash // [!include ~/snippets/async-multiselect.tsx] ```
Before production use, confirm these requirements: * Include all request context in `queryKey`. * Validate the option response at the API boundary. * Supply labels for initial selected IDs. * Test loading, error, empty, disabled, and read-only states. * Validate submitted IDs and authorization on the server. Read [Form kits](/form-kits) for the control contract. Read [Validation and submission](/validation) for the submit sequence. Read [Styling](/styling) for application-owned control styles. # Research grant application The form changes shape for individual and collective applicants. It also changes the settlement and reporting fields for the selected routes. ## Try the complete flow Choose a collective and the registered representation path. Search the fake registry and apply a result with typed `form.api` updates. Change the payout route to resolve another conditional branch. The research grant example runs in a browser. It combines branching identity, registry lookup, preserved conditional values, live preview, and two fake requests. Source: [docs-site/src/snippets/complex-research-grant.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/complex-research-grant.tsx) ## What this form demonstrates * Conditional fields across several nested objects. * An application-owned registry search inside the form. * Read-only values populated through `form.api.setValue`. * `useWatch` for conditional application UI and a live preview. * Two sequential mutations from transformed schema output. * Preserved values when the selected branch changes. ## Copy the source ```tsx twoslash // [!include ~/snippets/complex-research-grant.tsx] ``` Compare the other workflows in [Examples](/examples). # Creative studio policies This editor combines access, safeguards, age rules, equipment, food, connectivity, and animal access in one schema. ## Try the complete flow Change access or safeguard settings to reveal dependent fields. Add or reorder equipment rules. Enable all-ages access without a guardian rule, or set included connectivity below 25 Mbps, to see cross-section validation. The studio policy editor runs in a browser. It combines a loaded baseline with a catalog resource, cross-section rules, arrays, and two writes. Source: [docs-site/src/snippets/complex-studio-policies.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/complex-studio-policies.tsx) ## What this form demonstrates * A loaded policy baseline outside editable state. * A catalog resource passed through typed form context. * `matchResource` for pending, success, and error selectable options. * Saved option labels while the remote catalog is pending. * A generated equipment array and cross-section schema rules. * `useWatch` for a live policy summary. * Two writes from transformed submit output. TanStack Query owns request state. Form context exposes the current resource state to synchronous definition resolvers. ## Copy the source ```tsx twoslash // [!include ~/snippets/complex-studio-policies.tsx] ``` Compare the other workflows in [Examples](/examples). # Makerspace launch wizard The active screen is external React state. The form definition reads it through context, while React Hook Form stores only editable makerspace input. ## Try the complete flow Move through all four stages. On the location stage, edit the postal code and apply the asynchronous address result. Add or reorder capacity and gallery rows. The final submit validates the complete schema and performs three fake writes. The makerspace launch wizard runs in a browser. Four form-owned stages preserve address lookup, coordinates, media, pricing, and promotion state. Source: [docs-site/src/snippets/complex-makerspace-launch.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/complex-makerspace-launch.tsx) ## What this form demonstrates * Application-owned navigation passed through form context. * Context-provided campus and region options. * Address lookup driven by `useWatch`. * Typed coordinate updates from an application-owned control. * Native file input and generated arrays. * One transformed output used by three sequential mutations. "Form, Please" does not include a wizard engine. This example composes the workflow from external screen state and conditional sections. Put a stage in the schema only when the server stores it as a domain field, such as an approval stage. Do not put the current wizard screen in the schema only to drive UI. Use the [Product workflows tutorial](/workflows) as the canonical navigation, validation, progress, review, and first-invalid pattern. ## Copy the source ```tsx twoslash // [!include ~/snippets/complex-makerspace-launch.tsx] ``` Compare the other workflows in [Examples](/examples). # Learning cohort editor This form combines a loaded draft with session formats, price bands, media, and four enrollment offers. A fake title service returns suggestions for the current title. ## Try the complete flow Edit the title and choose a remote suggestion. Reorder session formats or create overlapping price bands. Enter `Reserved cohort` to see save-conflict feedback without losing the draft. The cohort editor runs in a browser. It combines remote title suggestions, configuration and price arrays, media, four offers, and conflict recovery. Source: [docs-site/src/snippets/complex-learning-cohort.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/complex-learning-cohort.tsx) ## What this form demonstrates * `useWatch` for a query key and a live preview. * `form.api.setValue` for an application-owned suggestion control. * Two generated arrays with cross-row schema validation. * Conditional offer fields that preserve hidden values. * Three sequential mutations from transformed submit output. ## Copy the source ```tsx twoslash // [!include ~/snippets/complex-learning-cohort.tsx] ``` Compare the other workflows in [Examples](/examples). # Membership ladder Four named tiers each contain a movable benefit array. The schema requires the discount values to increase across the tier ladder. ## Try the complete flow Change the discount values to test cross-tier validation. Add and move benefits inside each tier. Connect the selected workspace, then add a pause window with an application-owned shortcut. The membership ladder runs in a browser. Four nested tiers use cross-tier validation with workspace actions and pause-calendar shortcuts. Source: [docs-site/src/snippets/complex-membership-ladder.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/complex-membership-ladder.tsx) ## What this form demonstrates * Four nested arrays under fixed object paths. * Cross-tier schema validation. * Query-provided workspace options through form context. * `form.api.setValue` for connection state. * `form.api.getValues` and `form.api.setValue` for pause shortcuts. * A live ladder preview through `useWatch`. ## Copy the source ```tsx twoslash // [!include ~/snippets/complex-membership-ladder.tsx] ``` Compare the other workflows in [Examples](/examples). # Campaign builder Seven campaign templates share an audience, schedule, channel, and payment model. Each template shows a different payload section. ## Try the complete flow Switch between the loaded draft and a new campaign. Change the template to see the definition resolve a different section. Paid templates also show amount and recurring interval fields. The campaign builder runs in a browser. Seven templates share audience, schedule, payment, preserved conditional values, and create-or-edit network behavior. Source: [docs-site/src/snippets/complex-campaign-builder.tsx](https://github.com/r13v/form-please/blob/main/docs-site/src/snippets/complex-campaign-builder.tsx) ## What this form demonstrates * Conditional sections for seven schema branches. * Context-provided audience options. * Separate create and update mutations outside form state. * `useWatch` for the live campaign preview. * Transformed submit output with a derived channel list. * Preserved values when a conditional section becomes hidden. ## Copy the source ```tsx twoslash // [!include ~/snippets/complex-campaign-builder.tsx] ``` Compare the other workflows in [Examples](/examples). # API Use this page to find public imports, configuration, return values, and runtime behavior. Form, Please adds typed UI definitions to React Hook Form. Each form binding exposes the complete React Hook Form API as `form.api`. For exported TypeScript types and their relationships, read the [TypeScript reference](/types). All TypeScript examples on this page come from one file. The documentation build type-checks that file against the public package entries. ## Install Install the package, its required React Hook Form peer, and React: ```sh npm install form-please react-hook-form react react-dom ``` Import the optional layout styles when you use the supplied grid contract: ```ts import "form-please/layout.css" ``` ## Entry points | Import | Runtime exports | Purpose | | --- | --- | --- | | `form-please` | `defineControl`, `createFormKit`, `fromResource`, `matchResource`, `useSnapshot` | Build kits, controls, definitions, resource resolvers, and external-store adapters. | | `form-please/default-slots` | `createDefaultSlots` | Create accessible, unstyled structural slots. | | `form-please/history` | `createHistoryMiddleware`, `useHistory` | Add optional managed value history and journal transfer. | | `form-please/native-controls` | `createNativeControls` | Create the native HTML control registry. | | `form-please/persistence` | `createPersistenceMiddleware`, `createLocalStorageAdapter`, `createDateCodec`, `usePersistence` | Restore and autosave editable drafts through application storage. | | `form-please/preset-native` | `nativeFormKit` | Use the ready-made native kit with English slots. | | `form-please/preset-mui` | `createMuiFormKit` | Create a Material UI 9 kit. | `form-please/layout.css` and `form-please/package.json` are non-JavaScript exports. React UI, history, and persistence entries are client modules. ## `useSnapshot` `useSnapshot(store)` subscribes a component to any external store with stable `subscribe(listener)` and `getSnapshot()` functions. The snapshot type is inferred from `getSnapshot`. Its result must stay `Object.is`-equal until the store changes. The same getter supplies the server snapshot, so it must also be safe and consistent during server rendering. ```tsx // [!include ~/snippets/api-reference.tsx:use-snapshot] ``` ## `defineControl` `defineControl()` creates one control definition. Its generic parameters connect the control to compatible schema paths, application-owned props, selectable options, and form context. ```tsx // [!include ~/snippets/api-reference.tsx:define-control] ``` The control component receives these props: | Prop | Meaning | | --- | --- | | `path` | Current RHF field path. Array indexes use dot notation. | | `value` | Current field value. Its type is the first `defineControl` generic. | | `setValue(value)` | Replace the current field value. | | `blur()` | Mark the field as blurred. Call it from the interactive element. | | `input` | Generated `id`, `name`, focus `ref`, and optional `aria-describedby`. | | `meta` | Dirty, touched, validating, error, and displayed-error state. | | `props` | Deeply readonly application-owned props from the field definition. | | `options` | Current readonly collection for an option-capable control. | | `context` | Deeply readonly context from `useForm`. | | `disabled`, `readOnly` | Resolved form, parent, and field interaction state. | | `required` | Resolved presentation state. It does not add schema validation. | Attach all `input` properties to the interactive element. Call `blur` from its blur event. Use `meta.invalid` for `aria-invalid`. `meta.errors` contains all normalized field issues. `meta.displayErrors` contains the issues that the generated UI currently displays. Before the first submit, the UI displays issues only after the user touches the field. The function freezes the returned definition. It rejects `any`, `unknown`, and `never` as the control value type during type checking. ## `createFormKit` `createFormKit({ controls, slots, grid? })` creates and freezes one complete design-system contract. ```tsx // [!include ~/snippets/api-reference.tsx:create-form-kit] ``` | Option | Requirement | | --- | --- | | `controls` | A record of control definitions. Control names become definition values. | | `slots` | Components for fields, sections, arrays, array items, errors, and submit buttons. | | `grid` | Optional positive integer list. It must include `1` and contain no duplicates. | The default grid is `[1, 2, 3, 4]`. The function sorts a custom grid and freezes copies of the controls, slots, and grid. Add all controls before this call. A kit does not support runtime extension. The returned kit has these members: | Member | Result | | --- | --- | | `controls`, `slots`, `grid` | Frozen registry snapshots. | | `defineFragment` | Creates a reusable schema-owned UI fragment. | | `defineForm` | Creates a typed and normalized form definition. | | `forContext` | Returns a type-only context view of the same kit. | | `useForm` | Creates a component-local form binding. | | `Form`, `Fields`, `Submit`, `AutoForm` | Render the form and its generated UI. | ### `defineFragment` `kit.defineFragment(schema, (ui) => [...])` returns a frozen fragment with the exact input schema at `fragment.schema` and placement methods at `fragment.fields`. The object form `defineFragment(schema, { ui })` remains supported. Fragment-authored resolvers receive the local fragment input. See [Reusable fragments](/definitions#reusable-fragments) for composition, context, and array examples. ### Native controls `createNativeControls()` returns a fresh frozen registry on each call. | Control | Value type | Props and selectable options | | --- | --- | --- | | `text` | `string \| undefined` | `type`, `placeholder`, `autoComplete` | | `textarea` | `string \| undefined` | `placeholder`, `autoComplete`, `rows` | | `select` | `string \| undefined` | Field `options`; optional `props.emptyOption` | | `checkbox` | `boolean` | No props or options | | `number` | `number \| undefined` | `min`, `max`, `step`, `placeholder` | | `date` | `string \| undefined` | `min`, `max` | | `time` | `string \| undefined` | `min`, `max`, `step` | | `file` | `File \| undefined` | `accept` | Use `props.emptyOption` when a select value can be `undefined`. Do not also add an option whose value is an empty string. ```tsx // [!include ~/snippets/api-reference.tsx:native-control-options] ``` Import the option types from `form-please/native-controls`. The entry exports `NativeTextProps`, `NativeTextareaProps`, `NativeSelectProps`, `NativeNumberProps`, `NativeDateProps`, `NativeTimeProps`, and `NativeFileProps`. It also exports the related select and text types. ### Default slots `createDefaultSlots()` returns accessible field, section, array, array-item, error, and submit structure without a visual theme. ```tsx // [!include ~/snippets/api-reference.tsx:native-factories] ``` Use `i18n` to replace any English array action label: ```tsx // [!include ~/snippets/api-reference.tsx:default-slot-i18n] ``` | Key | Callback data | | --- | --- | | `arrayAdd` | `{ label }` from the array node | | `arrayRemove` | Zero-based `index` and one-based `position` | | `arrayMoveUp` | Zero-based `index` and one-based `position` | | `arrayMoveDown` | Zero-based `index` and one-based `position` | Each value can be a string or a callback that returns a string. Import `DefaultSlotsI18n` and its related types from `form-please/default-slots`. ### Native preset Use `nativeFormKit` when the native controls and default English slots are the required baseline. ```tsx // [!include ~/snippets/api-reference.tsx:native-preset] ``` Use the factories when the application needs localized slots or additional controls. ### Material UI preset Install Material UI 9 and Emotion before you import the preset: ```sh npm install @mui/material @emotion/react @emotion/styled ``` `createMuiFormKit()` returns a new frozen kit with Material UI controls, slots, and a 12-column grid. ```tsx // [!include ~/snippets/api-reference.tsx:mui-preset] ``` The `i18n` object supports `addItem`, `removeItem`, `moveItemUp`, `moveItemDown`, and `chooseFile`. All properties are optional. The registry contains these controls: | Value type | Control names | Props type | | --- | --- | --- | | `string \| undefined` | `text`, `textarea`, `password`, `email`, `url`, `tel`, `search`, `date`, `time`, `datetime-local` | `MuiTextFieldProps` | | `number \| undefined` | `number` | `MuiTextFieldProps` | | `string \| undefined` | `select` | `MuiSelectProps` | | `readonly string[]` | `select-multiple` | `MuiSelectMultipleProps` | | `string \| undefined` | `radio` | `MuiRadioProps` | | `boolean` | `checkbox`, `switch` | `MuiCheckboxProps` or `MuiSwitchProps` | | `string \| undefined` | `autocomplete` | `MuiAutocompleteProps` | | `readonly string[]` | `autocomplete-multiple` | `MuiAutocompleteMultipleProps` | | `File \| undefined` | `file` | `MuiFileProps` | | `readonly File[]` | `files` | `MuiFileProps` | | `number` | `slider` | `MuiSliderProps` | | `readonly number[]` | `range-slider` | `MuiRangeSliderProps` | Select, radio, and autocomplete field `options` narrow to the schema input union at the selected path. Multiple controls use the array element union. Custom select children and autocomplete `freeSolo` values remain outside that inference contract. ```tsx // [!include ~/snippets/api-reference.tsx:mui-fields] ``` The preset also exports its control props, option, slot-option, and localization types. The application owns `ThemeProvider`, `CssBaseline`, and theme configuration. See the [Material UI example](/examples/mui-yup) for a complete form. ## `defineForm` `kit.defineForm(schema, (ui) => [...])` validates, normalizes, and freezes a typed definition. The schema-bound `field`, `section`, `array`, and `render` helpers create ordinary nodes without requiring an authored `kind` property. The schema must implement Standard Schema. Field paths and control values use the schema input type, before schema transformations. ```tsx // [!include ~/snippets/api-reference.tsx:define-form] ``` `array.children` receives another builder bound to the item scope. The root builder and each array child callback run once while the definition is created; they are not value resolvers. The object form `defineForm(schema, { ui: [...] })` remains supported and produces the same normalized definition. In addition to opaque fragment placements, the UI array accepts four ordinary node kinds: | Kind | Required properties | Purpose | | --- | --- | --- | | `field` | `path`, `control` | Connect one schema input path to one registered control. | | `section` | `id`, `children` | Group nodes without changing their path scope. | | `array` | `path`, `itemDefault`, `children` | Repeat nodes in the scope of one array item. | | `render` | `id`, `component` | Insert form-local React content. | Field, section, and array nodes support resolved labels or descriptions. They also support `visible`, `disabled`, `readOnly`, `className`, `span`, and typed `slotOptions`. Fields add `required`, typed control `props`, and selectable `options` when the control supports them. Sections add `title` and `columns`. Each supported property except field `options` accepts a static value or a synchronous resolver. An ordinary node resolver receives the complete deeply readonly schema input and `{ context }`; a fragment-authored resolver receives its local fragment input. Form, Please resolves the complete tree after each value change. It throws when an ordinary resolver returns a promise. Field `options` may be an async `({ values, context, signal }) => options` function with tracked dependencies, cancellation, and `[]` fallback behavior. A request that rejects renders an empty collection. A resolver that successfully returns a non-array value violates its contract and throws a `TypeError` through React. Hidden fields keep their React Hook Form values. The `required` property changes the UI state only. Put every data rule in the schema. An array child path is relative to one item. `itemDefault` accepts a complete item or a function that returns one. Form, Please clones the value before each insertion. Array paths use their current numeric index, while React keys use RHF's stable field-array ID. A render component inherits resolved interaction state: ```tsx // [!include ~/snippets/api-reference.tsx:render-node] ``` The runtime rejects malformed schemas, unknown controls, invalid path syntax, duplicate node IDs, missing children, and grid values outside the kit grid. A definition belongs to the exact kit that created it. Keep the definition stable for the hook lifetime. To use another definition, remount the component with a different React `key`. ## `forContext` `kit.forContext()` returns a type-only view of the same runtime kit. It makes `context` mandatory in `useForm`. It also types resolvers and compatible controls. ```tsx // [!include ~/snippets/api-reference.tsx:context-kit] ``` The method does not create or clone a registry. Its context type must extend the current kit context. ## `useForm` `kit.useForm(definition, options)` creates a component-local `FormBinding`. | Option | Requirement and effect | | --- | --- | | `defaultValues` | Required complete synchronous schema input, fixed for the hook lifetime. | | `context` | Required after `forContext()`; otherwise optional. | | `disabled` | Disables generated controls and native form submission. | | `readOnly` | Locks generated controls but does not disable submission. | | `mode` | Optional RHF validation mode. Defaults to `onSubmit`. | | `reValidateMode` | Optional RHF revalidation mode. Defaults to `onChange`. | | `delayError` | Optional RHF error-display delay in milliseconds. | | `beforeUpdate` | Adjust or cancel one managed value proposal before middleware. | | `afterUpdate` | Observe the final committed transaction after middleware unwinds. | | `middleware` | Ordered Redux-shaped value middleware, fixed for the hook lifetime. | | `onSubmit` | Receives parsed output, editable input, the current binding, and a submitter snapshot. | Use `FormSubmitDetails` from `form-please` when a reusable submit handler needs the exact `{ value, input, form, submitter }` parameter type. | Property | Type | Source | | --- | --- | --- | | `value` | `FormOutput` | The successful Standard Schema parse. | | `input` | `FormInput` | A deep snapshot captured before validation. | | `form` | `FormBinding` | The binding that received the native submit. | | `submitter` | `Readonly<{ name: string; value: string }> \| null` | A frozen native submit-control snapshot captured before validation. | ```tsx // [!include ~/snippets/api-reference.tsx:use-form] ``` Submission has this sequence: 1. `kit.Form` captures the editable input and native submitter snapshots. 2. The internal RHF resolver parses that input once with the Standard Schema. 3. `onSubmit` receives `{ value, input, form, submitter }` after parsing succeeds. 4. A returned promise keeps `form.api.formState.isSubmitting` true until it settles. The definition schema is the only form-level resolver. "Form, Please" fixes `criteriaMode: "all"`, `shouldUnregister: false`, and RHF error focus. `submitter` is a frozen `{ name, value }` snapshot or `null` for an implicit submit. It is captured before validation and does not expose the live DOM element. Read the [product workflow tutorial](/workflows#understand-submitter) for named actions, async validation, and implicit-submit behavior. Use form-wide state for review or locked workflows: ```tsx // [!include ~/snippets/api-reference.tsx:form-wide-state] ``` ## `FormBinding` A binding contains these public fields: | Field | Purpose | | --- | --- | | `api` | Unchanged typed RHF `UseFormReturn` for values, fields, validation, reset, and subscriptions. | | `definition` | Fixed definition captured during the first hook render. | | `context` | Current runtime context. | | `update(recipe)` | Apply one managed Immer recipe through value middleware. | Form-wide interaction and focus bookkeeping remain private. Use `form.api` for raw RHF operations that deliberately bypass managed middleware. ## Managed update hooks Use `beforeUpdate` for one form-local rule that adjusts or cancels a managed proposal. Its Immer draft starts with the proposed `nextValues`. Mutate the draft and return nothing to continue, or return `false` to cancel the complete transaction. Use `afterUpdate` to observe the final `ValueTransaction` after commit and the synchronous middleware unwind. Its patches and `nextValues` include downstream middleware changes. ```tsx // [!include ~/snippets/api-reference.tsx:update-hooks] ``` The callbacks use their latest React render versions. Both must return synchronously and cannot start another managed update. An error from `beforeUpdate` prevents commit. An error from `afterUpdate` propagates without rolling committed values back. `afterUpdate` does not mean that React rendering or asynchronous validation has finished. Initial values, reset, direct `form.api` mutations, and application-owned `useFieldArray` operations bypass both hooks. Use middleware instead when several independent policies need explicit composition. Cancellation and effective no-op proposals do not call `afterUpdate`. ## Managed value middleware Use middleware when a generated change and its dependent values must commit as one proposal, or when work must run only after that proposal commits. Read [Value middleware](/middleware) for runnable examples, live previews, ordering, cancellation, bypasses, array constraints, and validation timing. ```tsx // [!include ~/snippets/api-reference.tsx:value-middleware] ``` Middleware has the Redux shape `api => next => transaction`. The public types are `FormMiddleware`, `FormMiddlewareApi`, `FormMiddlewareNext`, `ValueTransaction`, `ValueTransactionSource`, `ValuePatch`, and `FormUpdateRecipe`. A transaction contains deeply readonly TypeScript views of `previousValues` and derived `nextValues`. These views are not frozen or cloned as archival snapshots. The transaction also contains Immer `patches`, current `context`, and a discriminated `source`. Patch paths are segment arrays such as `["speakers", 0, "name"]`; they are not RHF dot paths. `next(patches)` reapplies those authoritative patches to `previousValues`, commits the resulting values synchronously, and returns the terminal transaction unless another middleware replaces that return value. Middleware can: * append or replace patches before calling `next`; * cancel the complete proposal by returning without `next`; * read final committed values and run post-change work after `next` returns; * return a promise after calling `next` synchronously. One middleware can call `next` synchronously at most once. It cannot start a nested `api.update` during an active transaction. A later update after awaited post-commit work is allowed. An exception before `next` prevents the commit; an exception after `next` does not roll it back. `form.update(recipe)` creates a managed transaction with source `update`. Its synchronous Immer recipe can mutate the draft or return a replacement, but not both. A no-op recipe creates no transaction. Redux middleware may replace the terminal return value, so `form.update` returns `unknown`. Generated controls and `form.update` publish final values with one RHF `setValues` call. Generated `append`, `remove`, and `move` preserve native `useFieldArray` row IDs; their dependent patches join the same final React render, although raw RHF subscribers can observe an intermediate array state. Array middleware cannot change the source array length or order beyond the source action. It must not change another generated array's structure. Only the proposed generated array action can change generated array length or order. `setValues` cannot synchronize the private row IDs in `useFieldArray`. Use each array's generated action for a managed structural change. Use a raw `useFieldArray` method only when the application deliberately bypasses middleware. Do not change generated array length or order from a generated control or `form.update`. These entry points cannot synchronize `useFieldArray` row IDs. Initial values, reset, and every direct `form.api` mutation bypass middleware. Managed updates cannot remove a top-level value because RHF shallow-merges roots; assign `undefined` when the schema permits it. Validation follows `mode` and `reValidateMode` after commit, but managed validation does not preserve `delayError`. RHF owns dirty and touched metadata. ## Managed value history `createHistoryMiddleware(options?)` and `useHistory(form, feature)` are exported only from `form-please/history`. Configure the returned feature in a form's middleware list, then pass it and the form to the hook. The hook result combines the exact form-specific handle with its reactive `snapshot`. | Option | Default | Effect | | --- | --- | --- | | `limit` | `100` | Maximum retained history groups; accepts a non-negative integer or `Infinity`. | | `groupWindow` | `750` | Milliseconds in which control changes on one path share a group; zero disables grouping. | The handle exposes `getSnapshot`, `subscribe`, `undo`, `redo`, `seek`, `clear`, `export`, and `import`. Navigation and import return `Promise`. `clear()` retains current values as the only position without changing the form. History restore enters hooks and middleware with source `history`. The action is `undo`, `redo`, `seek`, or `import`. Restore can remove optional top-level keys because its RHF reset terminal replaces complete values. Ordinary `form.update` keeps its existing top-level-removal restriction. `HistoryJournal` version 1 contains complete input entries and a numeric current index. Import checks the protocol and configured limit but does not require every editable entry to pass Standard Schema validation. Read [Managed value history](/history) for state ownership, grouping, raw RHF boundaries, restoration, and journal leaf behavior. ## Form persistence `createPersistenceMiddleware(options)` and `usePersistence(form, feature)` are exported only from `form-please/persistence`. Configure the feature in the fixed middleware list, then pass it and the form to the hook. The hook starts restore after mount and combines the exact handle with its reactive `snapshot`. | Option | Default | Effect | | --- | --- | --- | | `adapter` | Required | Keyed asynchronous `load`, `save`, and `remove` transport. | | `key` | Required | Application storage key. | | `version` | Required | Non-negative application data version. | | `saveDelay` | `500` | Trailing autosave delay in milliseconds. | | `codecs` | `[]` | Tagged encoders and decoders for explicit opaque values. | | `migrate` | None | Converts decoded data from another application version. | | `onError` | None | Observes failures with `PersistenceErrorDetails`, whose operation is `restore`, `save`, or `clear`. | The handle exposes `restore`, `start`, `flush`, `clear`, `getSnapshot`, and `subscribe`. Restore reports `"applied"`, `"empty"`, `"cancelled"`, `"transformed"`, or `"conflict"`. Read [Form persistence](/persistence) for lifecycle rules, envelope encoding, migrations, conflicts, and adapters. ## `Form`, `Fields`, `Submit`, and `AutoForm` | Component | Props and behavior | | --- | --- | | `kit.Form` | Requires a binding from the same kit. Accepts native form props except owned submit, reset, action, and validation props. | | `kit.Fields` | Renders the resolved definition. It renders optional children after generated fields. | | `kit.Submit` | Accepts button props except `type`. The configured submit slot always receives `type="submit"`. | | `kit.AutoForm` | Renders `Form`, the error summary, `Fields`, and then its children. | `Fields` and `Submit` must be descendants of the matching `kit.Form`. `AutoForm` provides that form context automatically. `Form` prevents native submission and connects the "Form, Please" submit wrapper to raw `form.api.handleSubmit`. A reset event calls `form.api.reset()`. It sets `noValidate` because the Standard Schema owns validation. `Submit` becomes disabled while validation or submission runs. It also follows the form-wide `disabled` option and its own `disabled` prop. Use RHF hooks and components directly when generated fields are only part of the screen. This example adds an application-owned field and watches generated values: ```tsx // [!include ~/snippets/api-reference.tsx:manual-composition] ``` `kit.Form` provides `form.api` through RHF `FormProvider`. `register`, `Controller`, `useController`, `useWatch`, `useFormState`, `useFieldArray`, and `useFormContext` therefore share the generated form's state. Application-owned fields must provide their own labels, descriptions, issue output, and focus behavior. Their paths must still exist in the definition schema, but they do not need a node in the definition. ## Resource helpers `matchResource` and `fromResource` handle the three `ResourceState` branches: `pending`, `success`, and `error`. ```tsx // [!include ~/snippets/api-reference.tsx:resource-states] ``` | Helper | Behavior | | --- | --- | | `matchResource(resource, cases)` | Selects one result immediately from a resource value. | | `fromResource(select, cases)` | Creates a synchronous UI resolver from application-owned resource state. | Every `fromResource` branch also receives the complete readonly form values and resolver details. ```tsx // [!include ~/snippets/api-reference.tsx:resource-resolver] ``` Use `matchResource` when you need a standalone value: ```tsx // [!include ~/snippets/api-reference.tsx:resources] ``` Both helpers require `pending`, `success`, and `error` branches. Neither helper fetches, caches, retries, cancels, or retains data. Read [Resource state](/resources) for a TanStack Query adapter. Read [Production recipes](/recipes) for complete composition and submission patterns. # TypeScript Import the types on this page from `form-please`. ```ts import type { FieldPath, FormInput, FormOutput } from "form-please" ``` This page covers every type exported from the root entry point. Types for native controls, default slots, and Material UI use their documented subpath entries. For functions, components, options, and runtime behavior, read the [API reference](/api). ## Choose the type you need Start with these types in application code: | Task | Type | | --- | --- | | Type form state and `defaultValues`. | `FormInput` | | Type a successful submit result. | `FormOutput` | | Accept a valid field name. | `FieldPath` | | Type shared form UI. | `UiNode` | | Type a reusable form fragment. | `FormFragment` | | Type a custom control component. | `ControlProps` | | Type form-local value middleware. | `FormMiddleware` | Most calls to `defineForm`, `useForm`, and `createFormKit` infer the remaining types. Add an explicit type when you export reusable configuration or build a library integration. ## Schema input and output `StandardSchema` is the common schema contract. Form, Please works with any library that implements this contract. `FormInput` is the editable value stored in React Hook Form. `FormOutput` is the parsed value passed to `onSubmit`. These types can be different when the schema transforms data. ```tsx // [!include ~/snippets/types-guide.tsx:schema-types] ``` Use the input type for `defaultValues`, field paths, controls, and resolvers. Use the output type after successful submission. Do not use the output type to describe form state. | Type | Purpose | | --- | --- | | `StandardSchema` | Describes a Standard Schema validator. `Output` defaults to `Input`. | | `FormInput` | Extracts the schema input type. | | `FormOutput` | Extracts the schema output type. | | `DeepReadonly` | Recursively models a readonly value. It does not freeze data at runtime. | ## Field paths Path types use the schema input shape and React Hook Form path syntax. Array indexes use dot segments, such as `contacts.0.email`. ```ts // [!include ~/snippets/types-guide.tsx:path-types] ``` | Type | Result | | --- | --- | | `FieldPath` | All valid string paths in `Value`. | | `PathValue` | The value at one valid path. | | `ArrayFieldPath` | RHF field-array paths whose items are objects. | Use `FieldPath` when a component or helper accepts any field. Add a second path generic when its value type must stay linked to the selected path. The `FieldUpdate` type in the example keeps this relationship. ## Definition and UI types `kit.defineForm()` accepts a UI tree and returns a normalized `FormDefinition`. Let the call infer the type for a definition that stays in one file. Use `UiNode` when the UI tree is declared separately: ```tsx // [!include ~/snippets/types-guide.tsx:ui-types] ``` The node generics carry the schema input, control registry, context, slot configuration, selectable options, and grid values through the complete tree. | Type | Purpose | | --- | --- | | `UiNode` | Union of ordinary field, section, array, and render source nodes. | | `FieldNode` | Field node with compatible paths, controls, props, and selectable options. | | `SectionNode` | Section node with typed child nodes. | | `ArrayNode` | Array node whose child paths are relative to one item. | | `RenderNode` | React component node with resolved interaction state. | | `FormFragment` | Schema-owned UI fragment created by `defineFragment`. | `FormDefinition` is the normalized, readonly result of `defineForm`. It contains the schema, grid, and normalized nodes. Do not create this object manually. Its generic parameters preserve the kit contracts. ### Resolver types A resolvable UI property accepts a static value or a synchronous resolver. Ordinary form resolvers receive the complete schema input and `{ context }`. Fragment-authored resolvers receive the local fragment input instead. ```ts type Visibility = UiResolver const canShowProfile: Visibility = (values, { context }) => context.canEdit && values.name.length > 0 ``` | Type | Purpose | | --- | --- | | `UiResolver` | Synchronous function that returns a derived UI value. | | `UiResolverValues` | Deeply readonly resolver values. | | `UiResolverDetails` | Object that contains deeply readonly `context`. | | `Resolvable` | Static `Value` or a `UiResolver` that returns it. | | `ReactUiContent` | A React element or string accepted by UI content properties. | `Resolvable` excludes a static function when `Value` is callable. This rule prevents Form, Please from confusing a component with a resolver. `DefaultGridValue` is the default grid union: `1 | 2 | 3 | 4`. A custom kit infers its grid union from `createFormKit({ grid })`. ### Render-node types `RenderNodeComponent` is a React component that receives `RenderNodeProps`. The props contain only `disabled` and `readOnly`. Use `RenderNode` to type the complete node. ## Control types `defineControl()` returns a typed `ControlDefinition`. The value type controls which schema fields can use the control. The context type controls which form kits can use it. ```tsx // [!include ~/snippets/types-guide.tsx:control-types] ``` | Type | Purpose | | --- | --- | | `ControlProps` | Props passed to a custom control component. | | `ControlDefinition` | Typed control returned by `defineControl`. | | `DefineControlInput` | Input object accepted by `defineControl`. | | `OptionValue` | Marks a control option value for narrowing to the selected schema input path. | | `FieldOptionsSource` | Static options or a tracked options function. | | `FieldOptionsResolver` | Function that returns options synchronously or asynchronously. | | `FieldOptionsResolverDetails` | Readonly `values`, `context`, and cancellation `signal`. | | `ControlDefinitionRegistry` | Readonly record of named control definitions. | | `AnyControlDefinition` | Non-specific control type used in registries. | Put `OptionValue` on the scalar option or on one property of a structured option. A field definition replaces the marker with the compatible schema input union at its `path`. The control receives `OwnProps` through `props` and the resolved collection through `options`. See [Option controls](/form-kits#option-controls) for a custom control and [Option values](/definitions#option-values) for definition-side inference. The extraction types read the hidden contract from a control definition: | Type | Extracts | | --- | --- | | `ControlValueOf` | Field value type. | | `ControlOwnPropsOf` | Definition-node `props` type. | | `ControlOptionOf` | One selectable option item. | | `ControlContextOf` | Required form context type. | Use these extraction types for wrappers and integration tests. Application forms usually rely on inference. ## Form-kit and binding types A form kit is the reusable design-system contract. A form binding is one component-local form instance. ```tsx // [!include ~/snippets/types-guide.tsx:form-types] ``` | Type | Purpose | | --- | --- | | `FormKit` | Kit methods, components, controls, slots, grid, and context contract. | | `CreateFormKitOptions` | Options accepted by `createFormKit`. | | `UseFormOptions` | `defaultValues`, context, flags, managed update hooks, value middleware, and submit handler. | | `FormSubmitDetails` | Parsed output, editable input, form binding, and submitter snapshot passed to a submit handler. | | `FormBinding` | Raw RHF API, managed `update`, fixed definition, and current context. | | `FormProps` | Props for `kit.Form`. | `AutoFormProps` is currently the same contract as `FormProps`. It is a separate public name for components that wrap `kit.AutoForm`. Use `form.api` from `FormBinding` for unchanged React Hook Form operations. Use `form.update` for a managed value transaction. The other public fields are the fixed `definition` and current `context`. Read [`FormBinding` on the API page](/api#formbinding) for their behavior. ### Managed value types Use these types when you configure value middleware or create a reusable managed update: | Type | Purpose | | --- | --- | | `FormUpdateRecipe` | Synchronous Immer recipe accepted by `form.update` and middleware `api.update`. | | `FormMiddleware` | Redux-shaped middleware for one form binding. | | `FormMiddlewareApi` | Read current values or start a later managed update. | | `FormMiddlewareNext` | Forward authoritative patches to the next middleware or terminal. | | `ValueTransaction` | TypeScript-readonly proposal or final committed update with previous values, next values, patches, source, and context. It is not a frozen snapshot. | | `ValueTransactionSource` | Discriminated source for a control, array action, `form.update`, or optional feature restore. | | `ValuePatch` | Readonly Immer operation with a segment-array path. | Middleware can replace the terminal return value. Therefore, `form.update` and `FormMiddlewareNext` return `unknown`. Read [Value middleware](/middleware) for runtime rules and examples. ### Managed value history types Import history types from `form-please/history`: | Type | Purpose | | --- | --- | | `CreateHistoryOptions` | Configure retention and control grouping. | | `HistoryFeature` | Reusable middleware plus exact-form handle lookup. | | `HistoryHandle` | Navigate, subscribe, clear, export, and import one form's history. | | `HistorySnapshot` | Current `canUndo`, `canRedo`, `index`, and `length`. | | `HistoryOperationResult` | `applied`, `unavailable`, `cancelled`, or `transformed`. | | `HistoryJournal` | Version 1 in-memory input positions and current index. | Read [Managed value history](/history) for the complete runtime contract. ### Form persistence types Import persistence types from `form-please/persistence`: | Type | Purpose | | --- | --- | | `JsonValue` | Value accepted by an adapter across its JSON boundary. | | `PersistenceCodec` | Tagged conversion for one explicit opaque value type. | | `PersistenceMigration` | Convert decoded input between application versions. | | `FormPersistenceAdapter` | Keyed asynchronous load, save, and remove transport. | | `CreatePersistenceOptions` | Configure the adapter, key, version, save delay, codecs, migration, and error observer. | | `PersistenceErrorDetails` | Identifies the failed `restore`, `save`, or `clear` operation. | | `PersistenceFeature` | Reusable middleware plus exact-form handle lookup. | | `PersistenceHandle` | Restore, start, flush, clear, and observe one form's draft. | | `PersistenceSnapshot` | Current restore phase and save state. | | `PersistenceRestoreResult` | `applied`, `empty`, `cancelled`, `transformed`, or `conflict`. | Read [Form persistence](/persistence) for the complete runtime contract. ## Slot and structure types `FormKitSlots` describes the six required slot components. The three option generics set the corresponding `slotOptions` prop types. ```tsx // [!include ~/snippets/types-guide.tsx:slot-types] ``` | Slot props type | Used by | | --- | --- | | `FieldSlotProps` | `Field` slot. | | `SectionSlotProps` | `Section` slot. | | `ArraySlotProps` | `Array` slot. | | `ArrayItemSlotProps` | `ArrayItem` slot. | | `ErrorMessageSlotProps` | `ErrorMessage` slot. | `SubmitSlotProps` types the `Submit` slot and typed render functions. It provides `buttonProps`, deeply readonly input `values`, `isSubmitting`, `isDirty`, and `canSubmit` (`!isValidating && !isSubmitting`). ### Shared structure types | Type | Purpose | | --- | --- | | `StructuralRootProps` | Generated attributes, style, and ref for a structural root element. | | `StructuralNodeName` | Allowed `data-fp-node` values for generated structure. | | `FormPleaseStyle` | React CSS properties plus the supported `--fp-*` variables. | | `FormIssue` | Normalized validation issue with `message` and optional `path`. | Spread `rootProps` on the slot's root element. Do not replace its generated attributes or ref. Read [Form kits](/form-kits) for every slot prop. ## Resource state `ResourceState` is a readonly tagged union. Its `status` is `"pending"`, `"success"`, or `"error"`. ```ts // [!include ~/snippets/types-guide.tsx:resource-type] ``` The type does not fetch or cache data. Use it to expose application-owned resource state. Read [Resource state](/resources) for mapping and adapter examples. # FAQs Use these answers to understand the boundaries between your application, React Hook Form, Standard Schema, and "Form, Please." ## Validation and submission ### Why does a valid submit parse the schema once? "Form, Please" supplies a small Standard Schema resolver to React Hook Form. The resolver validates the editable input and returns transformed output in the same parse. `kit.Form` captures the editable input and native submit control before raw RHF submission. The resolver then supplies transformed `FormOutput` to the same submit attempt. This sequence has four important effects: * Invalid input does not reach `onSubmit`. * `onSubmit` receives both the captured input and transformed output. * `onSubmit` receives a frozen submitter snapshot for named submit actions. * Direct `form.api.handleSubmit` remains ordinary RHF behavior. Keep schema validation deterministic and free of side effects. See [Validation and submission](/validation) for the complete validation sequence. ### What does `onSubmit` receive? The callback receives four values: * `value` is the transformed `FormOutput`. * `input` is the editable `FormInput` from React Hook Form. * `form` is the current "Form, Please" binding. * `submitter` is `{ name, value }` for the native submit control, or `null`. Use `value` for an API request. Use `input` when an operation needs the original editable shape. For example, a string input can produce a number output. The callback can return a promise. React Hook Form keeps `isSubmitting` true until that promise settles. See [Send schema output and reset with schema input](/recipes#send-schema-output-and-reset-with-schema-input) for a complete submission example. The submitter snapshot is captured before validation. It does not retain a DOM element. Read [Product workflows](/workflows#understand-submitter) for implicit submits and multiple validated actions. ### Does submission use native `FormData`? No. Submission uses the values in React Hook Form state. Nested objects, arrays, and `File` values keep their JavaScript types. Your application owns transport serialization. Send JSON, construct `FormData`, or use another protocol in `onSubmit`. "Form, Please" does not select a transport format. ```ts // [!include ~/snippets/production-recipes.tsx:multipart-body] ``` Pass the returned object as the `fetch` body. Do not set the `content-type` header because the browser must add the multipart boundary. ### Does `required: true` add a validation rule? No. The `required` property changes the control UI and accessibility state. The Standard Schema must reject a missing or invalid value. The same boundary applies to `visible`, `disabled`, and `readOnly`. These properties control the generated UI. They do not change the schema and do not remove values from submission. `kit.Form` disables native browser validation with `noValidate`. Put field and cross-field rules in the schema. ### How do I display server errors? Keep request failures in application state and render them near the form. A network failure is not a Standard Schema issue. If the server returns field-validation results, convert them deliberately with the applicable React Hook Form error APIs. Do not treat a transport failure as a schema result. The server must still validate every submission. See [Keep async submission in the application](/recipes#keep-async-submission-in-the-application) for a request error example. See [Apply server field errors](/recipes#apply-server-field-errors) for a typed `setError` example. ## Conditional UI and values ### Do hidden fields lose their values? No. A hidden generated field does not render, but its value remains in React Hook Form state. The Standard Schema still receives that value during validation and submission. This definition hides `companyName` for a personal account: ```tsx // [!include ~/snippets/lab-profile-form.tsx:conditional-field] ``` Match the schema condition to the visibility condition. Otherwise, an invalid hidden value can block submission. `kit.AutoForm` puts an issue for a hidden field in the error summary because there is no visible control to focus. Use a schema transform when the submitted output must omit an inactive value: ```tsx // [!include ~/snippets/conditional-fields-guide.tsx:conditional-schema] ``` Use value middleware when the editable value must be cleared atomically as the user changes another field. Use `form.update` for an application-owned managed action. A direct `form.api.setValue` bypasses middleware deliberately. See [Value middleware](/middleware) and [Conditional fields](/conditional-fields) for the boundaries. ### Does "Form, Please" fetch async options? Yes. A selectable field can use an async `options` function. "Form, Please" invokes it, tracks the `values` and `context` properties it reads, aborts the previous signal when those dependencies change, and ignores stale results. The function still contains the application request: ```tsx options: async ({ values, context, signal }) => loadCities(values.country, context.locale, signal) ``` The built-in policy uses `[]` during loading and after an uncaught error. It does not provide caching, retries, loading UI, error UI, search, or pagination. Use application resource state and `fromResource` or `matchResource` when you need those policies. A successfully resolved non-array value is a contract error, not an empty collection. See [Resource state](/resources). ### Can a UI resolver return a promise? Ordinary UI resolvers must return immediately. A promise-like result causes a runtime error. A selectable field's `options` function is the deliberate exception and may return a promise. Load data before or beside the form. Pass the current result through context. Resolvers can then derive labels, descriptions, control props, visibility, and interaction state synchronously. "Form, Please" resolves the complete UI tree after each form value change. Keep resolvers small and free of side effects. ## React Hook Form and manual UI ### How do I use React Hook Form directly? Use the API on `form.api`. This API is the typed React Hook Form instance that owns the editable state. Use `register`, `Controller`, or `useController` for custom fields. Use `useWatch`, `useFormState`, or `useFieldArray` for derived and array UI. Keep generated and manual UI inside the same `kit.Form`; it provides RHF `FormProvider` automatically. You can also call React Hook Form methods such as `setValue` and `reset` through `form.api`. See [Compose generated and custom UI](/recipes#compose-generated-and-custom-ui) for a complete example. Generated controls and `form.update` pass through configured value middleware. Direct `form.api` calls, reset, and initial values do not. Use raw RHF methods as an explicit escape hatch rather than expecting middleware to intercept them. ### Should I create a second form for custom UI? No. Create one binding with `kit.useForm`. Put `kit.Fields` and custom RHF components inside the same `kit.Form`. A second binding creates a second state owner. Changes, validation state, and submission state do not automatically synchronize between two bindings. ### What happens after an invalid submit? RHF focuses the first registered invalid field, including manual fields. Focus order follows RHF registration order. If no invalid generated control can receive focus, `kit.AutoForm` focuses the first error-summary item. A manually composed `kit.Form` does not add that summary fallback. The application must render its own summary when it does not use `kit.AutoForm`. ## Definitions, kits, and lifetime ### Can I replace a definition while the component stays mounted? No. `kit.useForm` keeps the first definition for the complete hook lifetime. Passing a different definition prop does not replace it. Define stable definitions at module scope when possible. To load another document or definition, remount the editor with a React `key`. Use `form.api.reset(nextInput)` when the definition stays the same and you want to install a new editable baseline deliberately. See [Load an edit-form baseline](/recipes#load-an-edit-form-baseline) for both operations. ### Can a definition use a different form kit? No. A definition belongs to the exact kit that created it. Pass it back to `useForm` on that kit. `kit.forContext()` is a type-only view of the same runtime kit. Use it to type resolver context and require a context value in `kit.useForm`. ```tsx // [!include ~/snippets/api-reference.tsx:context-kit] ``` When the application supplies a new context value, generated UI resolves with the current context and current form values. ### Can I add controls or slots to an existing kit? No. `createFormKit` takes one controls, slots, and grid snapshot. It freezes that configuration. Create the complete registries before you call `createFormKit`. Create another kit when the application needs a different registry or grid. ### Which schema libraries can I use? Use any schema that implements Standard Schema. "Form, Please" does not require a library-specific adapter. The documentation includes complete examples for [Zod](/get-started), [Yup](/examples/mui-yup), and [Valibot](/examples/shadcn-valibot). Field paths and controls always use the schema input type. `onSubmit.value` uses the schema output type.