React Hook Form
Formulários performáticos e flexíveis com re-renderizações mínimas - configuração, registro, Controller e padrões principais.
Busque em todas as páginas da documentação
Formulários performáticos e flexíveis com re-renderizações mínimas - configuração, registro, Controller e padrões principais.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
"use client";
import { useForm, Controller } from "react-hook-form";
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
function LoginForm() {
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<LoginForm>({
defaultValues: { email: "", password: "", rememberMe: false },
});
async function onSubmit(data: LoginForm) {
await fetch("/api/login", { method: "POST", body: JSON.stringify(data) });
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* register - para entradas nativas */}
<input {...register("email", { required: "Email é obrigatório" })} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register("password", { minLength: 8 })} />
{/* Controller - para componentes controlados */}
<Controller
name="rememberMe"
control={control}
render={({ field }) => (
<label>
<input type="checkbox" checked={field.value} onChange={field.onChange} />
Lembrar-me
</label>
)}
/>
<button type="submit" disabled={isSubmitting}>Entrar</button>
</form>
);
}Quando usar isso: Quando você precisa de um formulário com validação, tratamento de erros e bom desempenho - especialmente quando o formulário tem mais do que alguns campos.
"use client";
import { useForm, useFieldArray } from "react-hook-form";
interface Ingredient {
name: string;
amount: string;
}
interface RecipeFormData {
title: string;
description: string;
servings: number;
ingredients: Ingredient[];
}
export function RecipeEditor() {
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting, isDirty },
reset,
watch,
} = useForm<RecipeFormData>({
defaultValues: {
title: "",
description: "",
servings: 4,
ingredients: [{ name: "", amount: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "ingredients",
});
const watchTitle = watch("title");
function onSubmit(data: RecipeFormData) {
console.log("Receita:", data);
reset(data); // limpa isDirty
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-lg space-y-4">
<h2 className="text-lg font-bold">
{watchTitle || "Nova Receita"}
</h2>
<div>
<input
{...register("title", { required: "O título é obrigatório" })}
placeholder="Título da receita"
className="w-full rounded border p-2"
/>
{errors.title && <p className="text-sm text-red-600">{errors.title.message}</p>}
</div>
<textarea
{...register("description")}
placeholder="Descrição"
className="w-full rounded border p-2"
rows={3}
/>
<input
type="number"
{...register("servings", { valueAsNumber: true, min: 1 })}
className="w-32 rounded border p-2"
/>
<fieldset className="space-y-2">
<legend className="font-semibold">Ingredientes</legend>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<input
{...register(`ingredients.${index}.name`, { required: true })}
placeholder="Ingrediente"
className="flex-1 rounded border p-2"
/>
<input
{...register(`ingredients.${index}.amount`)}
placeholder="Quantidade"
className="w-24 rounded border p-2"
/>
<button type="button" onClick={() => remove(index)} className="text-red-500">
Remover
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "", amount: "" })}
className="text-sm text-blue-600"
>
+ Adicionar ingrediente
</button>
</fieldset>
<div className="flex gap-3">
<button
type="submit"
disabled={isSubmitting}
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
Salvar
</button>
{isDirty && <span className="self-center text-sm text-amber-600">Alterações não salvas</span>}
</div>
</form>
);
}O que isso demonstra:
useForm com defaultValues tipadasregister para entradas nativas com regras de validaçãouseFieldArray para listas dinâmicaswatch para observação de campos em tempo realisDirty e reset após salvarregister anexa ref, onChange, onBlur e name à entradahandleSubmit executa a validação primeiro e, em seguida, chama seu onSubmit apenas se for válidoformState (errors, isDirty, isValid, etc.) são avaliadas preguiçosamente via Proxy - apenas as propriedades acessadas causam re-renderizaçõesController conecta componentes controlados (seletores personalizados, seletores de data) ao RHFModos de validação:
const { register } = useForm({
mode: "onBlur", // valida ao perder o foco (padrão: "onSubmit")
reValidateMode: "onChange", // revalida ao mudar após o primeiro erro
});Definir valores programaticamente:
const { setValue, getValues, trigger } = useForm();
// Define um único campo
setValue("email", "novo@exemplo.com", { shouldValidate: true });
// Obtém todos os valores
const all = getValues();
// Dispara a validação manualmente
await trigger("email"); // campo único
await trigger(); // todos os camposValores padrão em nível de formulário de dados assíncronos:
const { reset } = useForm<ProfileForm>({
defaultValues: async () => {
const res = await fetch("/api/profile");
return res.json();
},
});// register fortemente tipado - captura erros de digitação em tempo de compilação
register("emial"); // Erro TS: "emial" não é uma chave de LoginForm
// Erros tipados
errors.email?.message; // string | undefined
// Tipo UseFormReturn para passar métodos de formulário como props
import type { UseFormReturn } from "react-hook-form";
function FormSection({ form }: { form: UseFormReturn<LoginForm> }) {
return <input {...form.register("email")} />;
}
// FieldPath para componentes de campo genéricos
import type { FieldPath, FieldValues } from "react-hook-form";
function TextInput<T extends FieldValues>({
name,
control,
}: {
name: FieldPath<T>;
control: Control<T>;
}) {
return <Controller name={name} control={control} render={({ field }) => <input {...field} />} />;
}Valores padrão devem ser completos - RHF usa defaultValues para determinar o estado inicial de isDirty. Omitir campos leva a um rastreamento incorreto de alterações. Correção: Sempre forneça todos os campos em defaultValues.
register retorna um ref - Não substitua o ref retornado por register. Correção: Use Controller se precisar de um ref personalizado ou mescle refs com um callback ref.
Re-renderizações de formState - Desestruturar formState no nível superior assina todas as propriedades. Correção: Desestruture apenas as propriedades de que você precisa: const { errors } = formState.
valueAsNumber retorna NaN para entradas vazias - Se a entrada estiver vazia, valueAsNumber: true retorna NaN. Correção: Combine com setValueAs ou use a coerção Zod via um resolver.
Chaves useFieldArray - Sempre use field.id como key, não o índice do array. Usar o índice causa bugs de estado ao reordenar ou remover itens.
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| FormData Nativo | Formulários de ação de servidor simples com lógica mínima do lado do cliente | Você precisa de validação em nível de campo e campos dinâmicos |
| Formik | Você está em uma base de código legada que já usa Formik | Iniciando um novo projeto (RHF tem melhor desempenho) |
useActionState do React 19 | Formulários com foco no servidor sem JavaScript do lado do cliente | Você precisa de validação instantânea de campos e UX complexa |
| Tanstack Form | Você deseja lógica de formulário independente de framework | Você precisa do ecossistema mais amplo de resolvers e bibliotecas de UI |
register funciona com entradas HTML nativas anexando ref, onChange, onBlur e nameController envolve componentes controlados (seletores personalizados, seletores de data) que precisam das props value e onChangeregister para entradas nativas; use Controller para componentes de terceiros ou personalizadosformState são avaliadas preguiçosamente via Proxy -- apenas as propriedades acessadas disparam re-renderizaçõesconst { errors } = formState em vez do objeto inteiroisDirty é true quando qualquer valor de campo difere de seus defaultValuesreset(data) após um salvamento bem-sucedido para atualizar a linha de base e definir isDirty como falsedefaultValues leva a um rastreamento incorreto de alteraçõesconst { reset } = useForm<ProfileForm>({
defaultValues: async () => {
const res = await fetch("/api/profile");
return res.json();
},
});defaultValues e o RHF a resolverá automaticamenteappend, remove, insert, move, swap, replace e updatefield.id estável para usar como chave do Reactregister retorna um ref que o RHF usa para rastrear o elemento de entradaController se precisar de um ref personalizado ou mescle refs com um callback refNumber("") retorna NaNvalueAsNumber: true usa essa conversão, resultando em NaN nos dados do formuláriosetValueAs ou use z.coerce.number() do Zod via um resolver em vez dissoconst { trigger } = useForm<FormData>();
await trigger("email"); // valida apenas o campo email
await trigger(); // valida todos os campostrigger aceita nomes de campo tipados do parâmetro genérico do formulárioimport type { UseFormReturn } from "react-hook-form";
function FormSection({ form }: { form: UseFormReturn<LoginForm> }) {
return <input {...form.register("email")} />;
}UseFormReturn<T> como o tipo da prop para obter segurança de tipo completa"onSubmit" (padrão): valida apenas quando o formulário é enviado"onBlur": valida quando um campo perde o foco"onChange": valida a cada pressionamento de teclareValidateMode controla o comportamento de revalidação após o primeiro errowatch("title") assina as alterações e dispara re-renderizações quando o valor mudagetValues("title") lê o valor atual sem assinar as alteraçõeswatch para atualizações reativas da UI; use getValues para leituras únicasRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥