Capítulo 88 de 108
Padrão oficial de integração de React Hook Form + Zod com os componentes Field/FieldGroup/FieldSet do shadcn/ui: usar <Controller /> para conectar cada input controlado e FieldError para exibir erro validado por schema.
useForm({ resolver: zodResolver(schema), defaultValues }): cria a instância do form; resolver conecta Zod (ou qualquer Standard Schema) à validação.<Controller name control render={({ field, fieldState }) => ...} />: padrão universal para conectar QUALQUER componente controlado (Input, Select, Checkbox, RadioGroup, Switch, Textarea) ao RHF.data-invalid={fieldState.invalid} no <Field /> + aria-invalid={fieldState.invalid} no controle: par obrigatório para estilização e acessibilidade de erro.<FieldError errors={[fieldState.error]} />: exibe a mensagem de erro do Zod associada ao campo.data-slot="checkbox-group" na <FieldGroup />: necessário para espaçamento/estilo correto de grupos de checkbox.mode do useForm: controla quando a validação dispara (onChange, onBlur, onSubmit [default], onTouched, all).useFieldArray({ control, name }): retorna fields, append, remove para campos de array dinâmicos.form.reset(): reseta para defaultValues.const formSchema = z.object({
title: z.string().min(5).max(32),
description: z.string().min(20).max(100),
})
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { title: "", description: "" },
})
<form onSubmit={form.handleSubmit(onSubmit)}>
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</form>
Controller, Field com data-invalid, controle com aria-invalid, FieldError condicional.// Checkbox array (múltipla seleção)
checked={field.value.includes(task.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, task.id]
: field.value.filter((value) => value !== task.id)
field.onChange(newValue)
}}
useFieldArray).// Select controlado
<Select name={field.name} value={field.value} onValueChange={field.onChange}>
<SelectTrigger aria-invalid={fieldState.invalid}>
<SelectValue placeholder="Select" />
</SelectTrigger>
...
</Select>
onChange nativo de input (Select, RadioGroup, Switch, Checkbox), usa-se value/onValueChange (ou checked/onCheckedChange) mapeados manualmente a field.value/field.onChange.const { fields, append, remove } = useFieldArray({ control: form.control, name: "emails" })
{fields.map((field, index) => (
<Controller
key={field.id}
name={`emails.${index}.address`}
control={form.control}
render={({ field: controllerField, fieldState }) => (/* ... */)}
/>
))}
<Button onClick={() => append({ address: "" })} disabled={fields.length >= 5}>Add</Button>
field.id (não index), e o name do Controller usa path indexado (emails.${index}.address).| Validation Mode | Comportamento |
|---|---|
onChange | Valida a cada mudança |
onBlur | Valida ao perder foco |
onSubmit (default) | Valida só no submit |
onTouched | Valida no primeiro blur, depois a cada mudança |
all | Valida em blur e change |
index como key em useFieldArray: deve ser field.id (id estável gerado pelo RHF), senão reordenação/remoção quebra o estado dos inputs.data-invalid/aria-invalid em par: sem os dois, o campo perde estilização de erro (data-invalid no Field) e acessibilidade (aria-invalid no controle).data-slot="checkbox-group" em grupos de checkbox: espaçamento sai errado.Controller é o conector universal do RHF para QUALQUER componente shadcn/ui controlado, não só inputs de texto — memorizar o padrão field/fieldState uma vez cobre Input, Textarea, Select, Checkbox, RadioGroup, Switch.Field/FieldSet/FieldLegend/FieldGroup/FieldContent dão total liberdade de markup, mas exigem disciplina manual de data-invalid/aria-invalid em cada campo.useFieldArray é o mecanismo padrão para listas dinâmicas de campos (ex: múltiplos e-mails), com append/remove e validação Zod aninhada via z.array(z.object({...})).form.formState.errors.<campo>.root, exigido renderizar FieldError separadamente para esse caso.form.reset() sem argumentos volta aos defaultValues originais.form.Field em vez de Controller.Field, FieldGroup, FieldSet, FieldLegend, FieldError) usada por todos os guias.