Form Groups
When building a multi-stage form that has many stages, like so:

It's common for each step to have its own form. However, this complicates the form submission and validation process by requiring you to add complex logic.
Luckily, TanStack Form provides a way to build out sub-forms that make this kind of development trivial to implement: <form.FormGroup>.
Usage#
To use a form group in TanStack Form, you'll use createForm or useAppForm to create a form variable, then reference its FormGroup component like you would a Field:
const form = createForm(() => ({
defaultValues: {
step1: {
name: '',
},
step2: {
age: 0,
},
},
}))
return (
<form.FormGroup name="step1">
{(group) => (
// `group()` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue`
// ...
)}
</form.FormGroup>
)This becomes much more useful when paired with external state to conditionally render a FormGroup:
const [step, setStep] = createSignal(0)
const form = createForm(() => ({
defaultValues: {
step1: {
name: '',
},
step2: {
age: 0,
},
},
}))
return (
<>
<Show when={step() === 0}>
<form.FormGroup
name="step1"
onGroupSubmit={() => {
// We can move the step forward when validation passes
setStep(step() + 1)
}}
onGroupSubmitInvalid={() => {
// Or handle invalid submissions, just like a top-level form
}}
onSubmitMeta={{} as SomeType}
>
{(group) => (
// Use `group().handleSubmit()` to submit the sub-form, but not the parent form
// ...
)}
</form.FormGroup>
</Show>
<Show when={step() === 1}>
<form.FormGroup name="step2">
{(group) => (
// Then, use `form.handleSubmit()` to submit the entire form
// ...
)}
</form.FormGroup>
</Show>
</>
)Form Group Validation#
Form groups have a distinct validation proceedure that we think makes sense for sub-forms:
- Form groups can have their own validation:
<form.FormGroup name="step1" validators={{ onChange: () => 'Error' }}>
{(group) => {
group().state.meta.errorMap // {onChange: "Error" | undefined}
group().state.meta.errors // ("Error")[]
}}
</form.FormGroup>- Can set errors on sub-fields:
<form.FormGroup
name="step1"
validators={{
onChange: ({ value, groupApi }) => ({
group: value.name === 'error' ? 'Group error' : undefined,
fields: {
// Must use the name of the field relative to the FormGroup as the error key,
// to stay consistent with how standard schema works with form groups
name: value.name === 'error' ? 'Field error' : undefined,
},
}),
}}
/>- And can even accept standard schemas:
<form.FormGroup
name="step1"
validators={{
onChange: z.object({
name: z.string().min(2),
}),
}}
/>The reason we don't use the full path names for fields is so that you can compose your schemas like so:
plaintextconst step1Schema = z.object({ name: z.string().min(2) }) const schema = z.object({ step1: step1Schema, step2: step2Schema })And pass the step1Schema to a form group and schema to the parent form. That way, partially validated data will still flag errors if the group is bypassed.
Dynamic Group Validation#
If you want to use dynamic validation (onDynamic) with a form group, do not rely on the onDynamic validator passed to createForm:
createForm(() => ({
validationLogic: revalidateLogic(),
validators: {
// This validator will not run `onChange` when a sub-form is submitted;
// it will only run `onChange` when the form itself is submitted.
onDynamic: schema,
},
}))Instead, pass your sub-schema for the group to the onDynamic validation of the FormGroup itself:
<form.FormGroup validators={{ onDynamic: step1Schema }} />It will treat group().submissionAttempts as the way to change what validator is ran before/after submit.
Form Group State#
Just like you're able to access group().state.meta.errors, you're also able to access the group's value using group().state.value. Likewise, here are some valuable properties you can access in the group().state.meta:
- group().state.meta.isFieldsValid: true when the field-level validators have no errors
- group().state.meta.isGroupValid: true when the group-level validators have no errors
- group().state.meta.isValid: true when both the field-level and group-level validators have no errors
- group().state.meta.isSubmitting: true when the group is in the process of being submitted