Formulários com React Hook Form + Zod
Um formulário de verdade tem validação, mensagem de erro e estado de envio — não só um onSubmit.
Um <form onSubmit> cru não valida nada, não mostra erro por campo, e reconstrói o objeto do zero a cada tecla se você tentar controlar tudo com useState. React Hook Form resolve o estado do formulário; Zod resolve a validação — e os dois se encaixam através de um "resolver".
1. O schema — a validação como dado, não como if/else
import { z } from "zod";
export const loginSchema = z.object({
email: z.string().email("E-mail inválido"),
senha: z.string().min(8, "Mínimo de 8 caracteres"),
});
export type LoginForm = z.infer<typeof loginSchema>;z.infer gera o tipo TypeScript a partir do schema — o tipo e a validação nunca ficam dessincronizados, porque são a mesma fonte.
2. O formulário — RHF cuida do estado, Zod valida
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema, type LoginForm } from "./schema";
export function LoginForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
});
async function onSubmit(data: LoginForm) {
await fetch("/api/login", { method: "POST", body: JSON.stringify(data) });
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3 max-w-sm">
<input {...register("email")} placeholder="E-mail" className="border rounded-lg px-3 py-2" />
{errors.email && <span className="text-sm text-red-600">{errors.email.message}</span>}
<input {...register("senha")} type="password" placeholder="Senha" className="border rounded-lg px-3 py-2" />
{errors.senha && <span className="text-sm text-red-600">{errors.senha.message}</span>}
<button disabled={isSubmitting} className="bg-blue-600 text-white rounded-lg px-4 py-2 disabled:opacity-50">
{isSubmitting ? "Entrando..." : "Entrar"}
</button>
</form>
);
}register("email") conecta o input ao RHF sem precisar de value/onChange manuais; errors.email.message já vem com a mensagem do próprio schema Zod; isSubmitting vem de graça, sem um useState extra pra loading.
Exercício
Adicione ao loginSchema um campo opcional lembrarDeMim do tipo booleano, e um checkbox correspondente no formulário.