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. onSubmitreceives both the captured input and transformed output.onSubmitreceives a frozen submitter snapshot for named submit actions.- Direct
form.api.handleSubmitremains 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:
valueis the transformedFormOutput<Schema>.inputis the editableFormInput<Schema>from React Hook Form.formis the current "Form, Please" binding.submitteris{ name, value }for the native submit control, ornull.
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.