Capítulo 88 de 108

React Hook Form

Core Idea

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.

Key Concepts

  • 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.

Code Examples

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>
  • O que demonstra: Anatomia completa do padrão de campo: schema Zod, 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)
}}
  • O que demonstra: Padrão de manipulação manual de array de valores para grupos de checkbox (RHF não tem helper nativo para isso, ao contrário de useFieldArray).
// Select controlado
<Select name={field.name} value={field.value} onValueChange={field.onChange}>
  <SelectTrigger aria-invalid={fieldState.invalid}>
    <SelectValue placeholder="Select" />
  </SelectTrigger>
  ...
</Select>
  • O que demonstra: Para componentes sem 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>
  • O que demonstra: Array fields dinâmicos: chave do map deve ser field.id (não index), e o name do Controller usa path indexado (emails.${index}.address).

Reference Tables

Validation ModeComportamento
onChangeValida a cada mudança
onBlurValida ao perder foco
onSubmit (default)Valida só no submit
onTouchedValida no primeiro blur, depois a cada mudança
allValida em blur e change

Anti-patterns

  • Usar 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.
  • Esquecer 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).
  • Não usar data-slot="checkbox-group" em grupos de checkbox: espaçamento sai errado.

Key Takeaways

  1. 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.
  2. Field/FieldSet/FieldLegend/FieldGroup/FieldContent dão total liberdade de markup, mas exigem disciplina manual de data-invalid/aria-invalid em cada campo.
  3. 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({...})).
  4. Erros de nível de array (não de item individual) aparecem em form.formState.errors.<campo>.root, exigido renderizar FieldError separadamente para esse caso.
  5. Para reset, form.reset() sem argumentos volta aos defaultValues originais.

Connects To

  • forms (ch087): índice geral de formulários.
  • forms/tanstack-form (ch089): alternativa com API baseada em form.Field em vez de Controller.
  • Field component: base de composição visual (Field, FieldGroup, FieldSet, FieldLegend, FieldError) usada por todos os guias.
  • Input Group: usado para inputs com botão de remoção inline (array fields).