form-please
v1.12.2
Published
Build forms with ease
Readme
Form, Please
Form, Please turns a Standard Schema and a typed UI definition into a React form. React Hook Form owns state, validation, subscriptions, and array updates. Your controls and slots own the rendered design system.
React 18 and React 19 are supported.
Install
npm install form-please react-hook-form zodForm, Please accepts any Standard Schema implementation. This example uses Zod.
Create a form
import { nativeFormKit as kit } from "form-please/preset-native"
import { z } from "zod"
const contactSchema = z
.object({
email: z.string().email("Enter a valid email"),
})
.transform((input) => ({ ...input, normalizedEmail: input.email.trim() }))
const contactForm = kit.defineForm(contactSchema, (ui) => [
ui.field("email", {
control: "text",
label: "Email",
props: { type: "email" },
}),
])
export function ContactForm() {
const form = kit.useForm(contactForm, {
defaultValues: { email: "" },
onSubmit({ value }) {
console.log(value.normalizedEmail)
},
})
return (
<kit.AutoForm form={form}>
<kit.Submit>Send</kit.Submit>
</kit.AutoForm>
)
}Render submit content with live typed values and state by passing a function to
kit.Submit. The required binding types the values and must match the
surrounding form; the button is yours:
<kit.Submit binding={form}>
{({ buttonProps, values, isDirty, canSubmit }) => (
<button {...buttonProps} disabled={!canSubmit || !isDirty}>
Save {values.name}
</button>
)}
</kit.Submit>Reuse a schema-owned group of fields at compatible object paths:
const addressSchema = z.object({
street: z.string(),
city: z.string(),
})
const addressFragment = kit.defineFragment(addressSchema, (ui) => [
ui.field("street", { control: "text", label: "Street" }),
ui.field("city", { control: "text", label: "City" }),
])
const checkoutSchema = z.object({
shippingAddress: addressFragment.schema,
billingAddress: addressFragment.schema,
})
const checkoutForm = kit.defineForm(checkoutSchema, () => [
addressFragment.fields({ at: "shippingAddress" }),
addressFragment.fields({ at: "billingAddress" }),
])Fragment resolvers receive the local fragment input. The host form still owns all state, validation, context, and lifecycle.
Use RHF register, Controller, useWatch, useFormState, useFieldArray,
and the unchanged form.api for direct composition. kit.Form supplies
FormProvider.
Runtime behavior
- The Standard Schema validates on submit, then on change after the first submit.
- The RHF resolver parses the Standard Schema once and returns transformed output while the submit wrapper preserves the schema input snapshot.
- Ordinary UI resolvers receive the complete deeply readonly schema input and
runtime context. They must be synchronous. A selectable field's
optionsmay instead be an async function of{ values, context, signal }. - Fragment resolvers receive their local deeply readonly fragment input and minimum context.
- Use definition-owned
beforeUpdateandafterUpdatefor one managed update rule shared by every binding of that definition. Use value middleware andform.updatewhen independent policies or dependent changes must compose atomically. Directform.apichanges bypass both. - Hidden fields preserve their values.
- Array paths use RHF dot notation and rows use stable RHF keys.
- RHF focuses the first registered invalid field. A focusable error summary is the fallback when no invalid field receives focus.
- A definition is fixed for the
useFormhook lifetime. Use a Reactkeyto remount with another definition.
Package entries
| Import | Purpose |
| --- | --- |
| form-please | Form-kit construction, controls, resources, and shared types |
| form-please/default-slots | Accessible structural slots and localization types |
| form-please/history | Optional managed value history, navigation, and journal transfer |
| form-please/native-controls | Native HTML controls and option types |
| form-please/persistence | Optional draft restore, autosave, migration, and storage adapters |
| form-please/preset-native | Ready-to-use native form kit |
| form-please/preset-mui | Material UI 9 form-kit factory |
| form-please/testing | Headless definition inspection and managed update testing |
| form-please/layout.css | Optional structural grid and spacing CSS |
The main JavaScript entry does not import CSS.
import "form-please/layout.css"The Material UI preset requires @mui/material, @emotion/react, and
@emotion/styled.
Resources
ResourceState, matchResource, and fromResource map application-owned
request state into synchronous form UI. For the common selectable-list case,
an async field options function gets dependency tracking and cancellation.
Form, Please does not add caching or retries.
Documentation
- Get started
- How it works
- Value middleware
- Managed value history
- Form persistence
- API reference
- Troubleshooting
- Shadcn registry adapter
- Architecture map
- Use with AI agents
- LLM documentation index
- Full documentation for LLMs
The physical, typechecked example lives in
profile-form.tsx.
Kudos
Kudos to Evgeniy Ivaha for the idea and the example implementation.
