Skip to content
Form, Please

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<Schema> 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 for the complete validation sequence.

What does onSubmit receive?

The callback receives four values:

  • value is the transformed FormOutput<Schema>.
  • input is the editable FormInput<Schema> 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 for a complete submission example.

The submitter snapshot is captured before validation. It does not retain a DOM element. Read Product workflows 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.

const uploadSchema = z.object({
	title: z.string().trim().min(1),
	attachment: z.file().optional(),
})
 
function createUploadBody(value: FormOutput<typeof uploadSchema>): FormData {
	const body = new FormData()
	body.set("title", value.title)
	if (value.attachment !== undefined) {
		body.set("attachment", value.attachment)
	}
	return 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 for a request error example. See 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:

				{
					kind: "field",
					path: "companyName",
					control: "text",
					label: "Company name",
					required: ({ accountType }) => accountType === "company",
					visible: ({ accountType }) => accountType === "company",
					props: {
						placeholder: "Compiler Labs",
						autoComplete: "organization",
					},
				},

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:

const accountSchema = z
	.object({
		accountType: z.enum(["personal", "company"]),
		companyName: z.string().optional(),
	})
	.superRefine((value, context) => {
		if (
			value.accountType === "company" &&
			(value.companyName ?? "").trim().length === 0
		) {
			context.addIssue({
				code: "custom",
				message: "Company name is required",
				path: ["companyName"],
			})
		}
	})
	.transform(({ companyName, ...account }) => {
		if (account.accountType === "personal") return account
		return { ...account, companyName: companyName?.trim() }
	})

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 and 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:

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.

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 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 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<Context>() is a type-only view of the same runtime kit. Use it to type resolver context and require a context value in kit.useForm.

const profileKit = kit.forContext<ProfileContext>()

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, Yup, and Valibot. Field paths and controls always use the schema input type. onSubmit.value uses the schema output type.