Capítulo 89 de 108
Padrão de integração de TanStack Form + Zod com Field/FieldGroup/FieldSet, usando form.Field (render-prop / children) em vez do <Controller /> do React Hook Form. API headless, validação declarada via validators no useForm.
useForm({ defaultValues, validators: { onSubmit: schema }, onSubmit }): cria a instância; onSubmit recebe { value } já validado.<form.Field name="x" children={(field) => ...} />: equivalente ao <Controller /> do RHF; expõe field.state.value, field.state.meta.isTouched, field.state.meta.isValid, field.state.meta.errors, field.handleChange, field.handleBlur.isInvalid = field.state.meta.isTouched && !field.state.meta.isValid: padrão consistente para decidir quando mostrar erro (diferente do RHF, que usa fieldState.invalid direto).form.handleSubmit() dentro de onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}: TanStack Form não integra automaticamente com o evento nativo do <form>, é chamada manual.mode="array": em form.Field, habilita array fields; expõe field.state.value (array), field.pushValue(item), field.removeValue(index).name={emails[${index}].address} (diferente do RHF que usa ponto: emails.${index}.address).const form = useForm({
defaultValues: { title: "", description: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => { /* ... */ },
})
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
<form.Field
name="title"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
</form>
form.Field, incluindo o isInvalid calculado a partir de isTouched/isValid (não existe um .invalid pronto como no RHF).<form.Field name="emails" mode="array">
{(field) => (
<FieldSet>
{field.state.value.map((_, index) => (
<form.Field
key={index}
name={`emails[${index}].address`}
children={(subField) => (/* Field com subField.state.value/handleChange */)}
/>
))}
<Button onClick={() => field.pushValue({ address: "" })} disabled={field.state.value.length >= 5}>
Add Email Address
</Button>
</FieldSet>
)}
</form.Field>
mode="array" + pushValue/removeValue como equivalente do useFieldArray do RHF, sem hook separado.| Validation Mode | Descrição |
|---|---|
onChange | Valida a cada mudança |
onBlur | Valida ao perder foco |
onSubmit | Valida no submit |
Configurado via validators: { onSubmit: schema, onChange: schema, onBlur: schema } (pode combinar múltiplos).
index como key em vez de estabilizar por item: a doc usa key={index} aqui (diferente do RHF que exige field.id), pois TanStack Form não gera IDs estáveis — atenção redobrada se a lista permitir reordenação (index como key pode causar bugs de estado em reorder, mesmo que a doc oficial use assim para add/remove simples).e.preventDefault() + form.handleSubmit() manual: sem isso o form não submete (TanStack Form não se liga automaticamente ao evento submit nativo).form.Field com children/render-prop substitui o <Controller /> do RHF; a "forma" de pensar (campo controlado via field.state.value/handleChange) é a mesma ideia.isTouched, ao contrário do padrão RHF que usa fieldState.invalid diretamente (RHF já considera modo de validação internamente).mode="array" + pushValue/removeValue na própria API do form.Field, sem hook adicional.emails[0].address), não ponto.Controller-based em vez de render-prop.