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 useForm or useAppForm to create a form variable, then reference its FormGroup component like you would a Field:
const form = useForm({
defaultValues: {
step1: {
name: ""
},
step2: {
age: 0
}
}
})
return (
<form.FormGroup
name="step1"
children={group => (
// `group` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue`
// ...
)}
/>
)This becomes much more useful when paired with external state to conditionally render a FormGroup:
const [step, setStep] = useState(0)
const form = useForm({
defaultValues: {
step1: {
name: ""
},
step2: {
age: 0
}
}
})
return (
<>
{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}
children={group => (
// Use `group.handleSubmit()` to submit the sub-form, but not the parent form
// ...
)}
/> : null }
{step === 1 ? <form.FormGroup
name="step2"
children={group => (
// Then, use `form.handleSubmit()` to submit the entire form
// ...
)}
/> : null }
</>
)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' }}
children={(group) => {
group.state.meta.errorMap // {onChange: "Error" | undefined}
group.state.meta.errors // ("Error")[]
}}
/>- 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 useForm:
useForm({
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