react-hook-form | React
- react-hook-form | React
React Hook Form — Documentação prática e completa
1) O que é o React Hook Form?
O React Hook Form é uma biblioteca para formulários em React focada em:
- Alta performance (menos re-render)
- API simples
- Validação poderosa
- Suporte excelente para inputs não controlados
- Integração fácil com validadores como Zod / Yup
- Suporte nativo para campos dinâmicos (arrays)
2) Instalação
npm i react-hook-form
Opcional com Zod:
npm i zod @hookform/resolvers
Opcional com Yup:
npm i yup @hookform/resolvers
3) Conceitos principais (o que você PRECISA saber)
useForm()
É o hook principal do RHF. Ele te dá as ferramentas do formulário:
register()→ registra campos simples (input, select, textarea)handleSubmit()→ trata envio com validaçãoformState.errors→ erros por campowatch()→ observa valoressetValue()→ altera valor manualmentegetValues()→ pega valores atuaisreset()→ reseta valorestrigger()→ valida manualmentecontrol→ usado comControllersetError()/clearErrors()→ controlar erros na mão
4) Exemplo MAIS básico (input + submit)
import { useForm } from "react-hook-form";
type FormData = {
name: string;
};
export function BasicForm() {
const { register, handleSubmit } = useForm<FormData>();
function onSubmit(data: FormData) {
console.log("Dados:", data);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input placeholder="Nome" {...register("name")} />
<button type="submit">Enviar</button>
</form>
);
}
5) Validação básica (required, minLength, pattern...)
import { useForm } from "react-hook-form";
type FormData = {
email: string;
password: string;
};
export function ValidationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
return (
<form
onSubmit={handleSubmit((data) => {
console.log(data);
})}
>
<input
placeholder="Email"
{...register("email", {
required: "Email é obrigatório",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Email inválido",
},
})}
/>
{errors.email && <p>{errors.email.message}</p>}
<input
placeholder="Senha"
type="password"
{...register("password", {
required: "Senha obrigatória",
minLength: { value: 6, message: "Mínimo 6 caracteres" },
})}
/>
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Entrar</button>
</form>
);
}
✅ Validadores mais usados:
requiredminLengthmaxLengthminmaxpatternvalidate(custom)
6) Validação custom (função validate)
<input
{...register("username", {
validate: (value) => {
if (value.toLowerCase() === "admin") return "Nome reservado";
return true;
},
})}
/>
✅ Pode retornar:
true→ válido"mensagem"→ inválido
7) Validar múltiplas regras no validate
<input
{...register("password", {
validate: {
hasUppercase: (v) => /[A-Z]/.test(v) || "Deve ter 1 letra maiúscula",
hasNumber: (v) => /\d/.test(v) || "Deve ter 1 número",
},
})}
/>
8) watch() (assistir valores em tempo real)
const { watch, register } = useForm<{ name: string }>();
const name = watch("name");
return (
<>
<input {...register("name")} />
<p>Digitando: {name}</p>
</>
);
✅ watch() também aceita múltiplos:
const [email, password] = watch(["email", "password"]);
9) defaultValues (muito importante)
const { register } = useForm({
defaultValues: {
name: "João",
email: "",
},
});
✅ Isso define o valor inicial do formulário.
10) reset() (resetar formulário)
Resetar tudo:
reset();
Resetar para novos valores:
reset({ name: "Maria", email: "maria@email.com" });
✅ Exemplo real pós submit:
const onSubmit = (data) => {
console.log(data);
reset();
};
11) setValue() e getValues() (controlar na mão)
const { setValue, getValues } = useForm();
setValue("name", "Carlos");
console.log(getValues("name"));
✅ Forçar validação ao setar:
setValue("name", "Carlos", { shouldValidate: true });
12) trigger() (validar manualmente)
Validar tudo:
await trigger();
Validar só um campo:
await trigger("email");
13) setError() (erro vindo do backend / API)
Exemplo: API diz que email já existe:
setError("email", {
type: "server",
message: "Esse email já está em uso",
});
Limpar erro:
clearErrors("email");
Exemplo completo de formulário real (cadastro)
import { useForm } from "react-hook-form";
type RegisterForm = {
name: string;
email: string;
password: string;
age: number;
};
export function FullRegisterForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegisterForm>();
async function onSubmit(data: RegisterForm) {
console.log(data);
await new Promise((r) => setTimeout(r, 1000));
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
placeholder="Nome"
{...register("name", { required: "Nome obrigatório" })}
/>
{errors.name && <p>{errors.name.message}</p>}
<input
placeholder="Email"
{...register("email", { required: "Email obrigatório" })}
/>
{errors.email && <p>{errors.email.message}</p>}
<input
placeholder="Senha"
type="password"
{...register("password", {
required: "Senha obrigatória",
minLength: { value: 6, message: "Min 6" },
})}
/>
{errors.password && <p>{errors.password.message}</p>}
<input
placeholder="Idade"
type="number"
{...register("age", {
valueAsNumber: true,
min: { value: 18, message: "Precisa ser maior de idade" },
})}
/>
{errors.age && <p>{errors.age.message}</p>}
<button disabled={isSubmitting} type="submit">
{isSubmitting ? "Enviando..." : "Cadastrar"}
</button>
</form>
);
}
Tipos de campos (todos exemplos úteis)
1) Checkbox
<input type="checkbox" {...register("terms", { required: true })} />
Erros:
{errors.terms && <p>Aceite os termos</p>}
2) Radio
<label>
<input type="radio" value="M" {...register("gender")} /> Masculino
</label>
<label>
<input type="radio" value="F" {...register("gender")} /> Feminino
</label>
3) Select
<select {...register("role", { required: "Selecione um cargo" })}>
<option value="">Selecione</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
4) Textarea
<textarea {...register("bio")} />
Controller (quando register NÃO serve)
Você usa Controller quando o componente:
- não aceita
ref - é um input “controlado”
- é de UI lib (MUI, Antd, Chakra, Radix, etc)
- trabalha com
valueeonChangemanual
Exemplo com Controller
import { useForm, Controller } from "react-hook-form";
type FormData = { name: string };
export function ControllerExample() {
const { control, handleSubmit } = useForm<FormData>({
defaultValues: { name: "" },
});
return (
<form onSubmit={handleSubmit(console.log)}>
<Controller
name="name"
control={control}
rules={{ required: "Obrigatório" }}
render={({ field, fieldState }) => (
<>
<input {...field} placeholder="Nome" />
{fieldState.error && <p>{fieldState.error.message}</p>}
</>
)}
/>
<button type="submit">Enviar</button>
</form>
);
}
Integração com Zod (recomendado)
1) Schema
import { z } from "zod";
export const schema = z.object({
name: z.string().min(1, "Nome obrigatório"),
email: z.string().email("Email inválido"),
password: z.string().min(6, "Min 6"),
});
2) Resolver
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
type FormData = z.infer<typeof schema>;
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
✅ Form completo:
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
name: z.string().min(1, "Nome obrigatório"),
email: z.string().email("Email inválido"),
password: z.string().min(6, "Min 6"),
});
type FormData = z.infer<typeof schema>;
export function ZodForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(console.log)}>
<input placeholder="Nome" {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
<input placeholder="Email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" placeholder="Senha" {...register("password")} />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Salvar</button>
</form>
);
}
Integração com Yup (alternativa)
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
const schema = yup.object({
name: yup.string().required("Obrigatório"),
email: yup.string().email("Inválido").required(),
});
const { register } = useForm({
resolver: yupResolver(schema),
});
Campos dinâmicos com useFieldArray (listas)
Exemplo: adicionar múltiplos telefones.
import { useForm, useFieldArray } from "react-hook-form";
type FormData = {
phones: { number: string }[];
};
export function FieldArrayExample() {
const { control, register, handleSubmit } = useForm<FormData>({
defaultValues: { phones: [{ number: "" }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: "phones",
});
return (
<form onSubmit={handleSubmit(console.log)}>
{fields.map((field, index) => (
<div key={field.id}>
<input
placeholder="Telefone"
{...register(`phones.${index}.number` as const, {
required: "Obrigatório",
})}
/>
<button type="button" onClick={() => remove(index)}>
Remover
</button>
</div>
))}
<button type="button" onClick={() => append({ number: "" })}>
Adicionar telefone
</button>
<button type="submit">Enviar</button>
</form>
);
}
✅ Ações possíveis:
appendprependremoveswapmoveinsertreplace
Upload de arquivo (File)
type FormData = {
avatar: FileList;
};
export function UploadExample() {
const { register, handleSubmit } = useForm<FormData>();
return (
<form
onSubmit={handleSubmit((data) => {
const file = data.avatar?.[0];
console.log(file);
})}
>
<input type="file" {...register("avatar")} />
<button type="submit">Enviar</button>
</form>
);
}
✅ Validar tamanho:
<input
type="file"
{...register("avatar", {
validate: (files) => {
const file = files?.[0];
if (!file) return "Arquivo obrigatório";
if (file.size > 2_000_000) return "Máximo 2MB";
return true;
},
})}
/>
Máscara (CPF / Telefone) — melhor abordagem
Com máscara geralmente você usa:
Controller+ input mascarado (lib externa) OU- tratar valor no
onChange
Exemplo simples de máscara manual (telefone)
function maskPhone(value: string) {
return value
.replace(/\D/g, "")
.replace(/^(\d{2})(\d)/, "($1) $2")
.replace(/(\d{5})(\d)/, "$1-$2")
.slice(0, 15);
}
Uso:
<input
{...register("phone", {
onChange: (e) => {
e.target.value = maskPhone(e.target.value);
},
})}
/>
✅ Em produção, normalmente se usa Controller com lib de máscara.
Submit com async + loading + desabilitar botão
const { handleSubmit, formState: { isSubmitting } } = useForm();
<button disabled={isSubmitting}>
{isSubmitting ? "Carregando..." : "Enviar"}
</button>
Erros gerais do form (não só por campo)
Exemplo: API falhou e você quer mostrar uma mensagem no topo:
setError("root", { message: "Erro ao salvar dados" });
Exibir:
{errors.root && <p>{errors.root.message}</p>}
Form em múltiplas etapas (Wizard)
Estratégia: um useForm para tudo, mas renderiza por etapas.
import { useState } from "react";
import { useForm } from "react-hook-form";
type FormData = {
name: string;
email: string;
password: string;
};
export function WizardForm() {
const [step, setStep] = useState(1);
const {
register,
handleSubmit,
trigger,
formState: { errors },
} = useForm<FormData>();
async function next() {
if (step === 1) {
const ok = await trigger("name");
if (!ok) return;
}
if (step === 2) {
const ok = await trigger("email");
if (!ok) return;
}
setStep((s) => s + 1);
}
return (
<form onSubmit={handleSubmit(console.log)}>
{step === 1 && (
<>
<input {...register("name", { required: "Nome obrigatório" })} />
{errors.name && <p>{errors.name.message}</p>}
<button type="button" onClick={next}>Próximo</button>
</>
)}
{step === 2 && (
<>
<input {...register("email", { required: "Email obrigatório" })} />
{errors.email && <p>{errors.email.message}</p>}
<button type="button" onClick={next}>Próximo</button>
</>
)}
{step === 3 && (
<>
<input
type="password"
{...register("password", { required: "Senha obrigatória" })}
/>
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Finalizar</button>
</>
)}
</form>
);
}
Boas práticas (as que mais importam)
✅ Prefira register() sempre que possível
✅ Use Controller só quando necessário
✅ Use resolver (Zod/Yup) para validação consistente
✅ Use defaultValues sempre (evita warning e bugs)
✅ Use valueAsNumber para inputs numéricos
✅ Use formState.isSubmitting para UX correta
✅ Use setError para erros vindos do backend
"Todos os exemplos possíveis" (resumo do que você tem aqui)
Você já tem exemplos de:
✅ Form básico ✅ Required / min / max / regex ✅ validate custom ✅ validate múltiplo ✅ watch (simples e múltiplo) ✅ defaultValues ✅ reset ✅ setValue / getValues ✅ trigger ✅ setError / clearErrors ✅ checkbox / radio / select / textarea ✅ Controller ✅ Zod resolver ✅ Yup resolver ✅ useFieldArray (campos dinâmicos) ✅ upload + validação de arquivo ✅ máscara simples ✅ wizard multi-step ✅ submit async + loading