Skip to main content

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ção
  • formState.errors → erros por campo
  • watch() → observa valores
  • setValue() → altera valor manualmente
  • getValues() → pega valores atuais
  • reset() → reseta valores
  • trigger() → valida manualmente
  • control → usado com Controller
  • setError() / 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:

  • required
  • minLength
  • maxLength
  • min
  • max
  • pattern
  • validate (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 value e onChange manual

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:

  • append
  • prepend
  • remove
  • swap
  • move
  • insert
  • replace

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