Por Que React Hook Form e Zod Juntos?
Quando você trabalha com formulários complexos em React, rapidamente percebe que gerenciar estado, validação e submissão de forma manual é tedioso e propenso a erros. React Hook Form resolve o gerenciamento de estado com performance excepcional, mantendo o formulário desacoplado do componente. Zod é uma biblioteca de validação TypeScript-first que oferece type safety nativo e mensagens de erro estruturadas. Juntas, elas eliminam boilerplate e reduzem bugs em produção.
A combinação é poderosa porque React Hook Form é agnóstica sobre validação (você escolhe a estratégia), enquanto Zod fornece schemas declarativos e reutilizáveis. Você define as regras uma única vez e as aproveita tanto no cliente quanto no servidor.
Configuração Inicial e Integração
Instalação das Dependências
npm install react-hook-form zod @hookform/resolvers
O pacote @hookform/resolvers é essencial — ele adapta Zod ao padrão esperado por React Hook Form.
Exemplo Básico de Formulário
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Schema Zod
const userSchema = z.object({
name: z.string().min(3, 'Nome deve ter no mínimo 3 caracteres'),
email: z.string().email('Email inválido'),
password: z.string().min(8, 'Senha deve ter no mínimo 8 caracteres'),
});
type UserFormData = z.infer<typeof userSchema>;
export function UserForm() {
const { register, handleSubmit, formState: { errors } } = useForm<UserFormData>({
resolver: zodResolver(userSchema),
});
const onSubmit = (data: UserFormData) => {
console.log('Dados válidos:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('name')} placeholder="Nome" />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<input {...register('email')} type="email" placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input {...register('password')} type="password" placeholder="Senha" />
{errors.password && <span>{errors.password.message}</span>}
</div>
<button type="submit">Enviar</button>
</form>
);
}
O método register vincula campos ao formulário, enquanto handleSubmit valida antes de chamar onSubmit. Os erros vêm pré-formatados do schema Zod.
Validações Avançadas com Zod
Validações Condicionais e Refinamentos
Formulários reais exigem lógica além de tipos básicos. Zod oferece .refine() e .superRefine() para isso:
const registroSchema = z.object({
email: z.string().email(),
confirmarEmail: z.string().email(),
tipoUsuario: z.enum(['pessoal', 'empresarial']),
cnpj: z.string().optional(),
}).refine((data) => data.email === data.confirmarEmail, {
message: 'Emails não correspondem',
path: ['confirmarEmail'], // Vincula o erro ao campo específico
}).refine((data) => {
if (data.tipoUsuario === 'empresarial' && !data.cnpj) {
return false;
}
return true;
}, {
message: 'CNPJ obrigatório para empresas',
path: ['cnpj'],
});
export function RegistroForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(registroSchema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('email')} placeholder="Email" />
<input {...register('confirmarEmail')} placeholder="Confirme o email" />
{errors.confirmarEmail && <span>{errors.confirmarEmail.message}</span>}
<select {...register('tipoUsuario')}>
<option value="pessoal">Pessoal</option>
<option value="empresarial">Empresarial</option>
</select>
<input {...register('cnpj')} placeholder="CNPJ (se empresa)" />
{errors.cnpj && <span>{errors.cnpj.message}</span>}
<button type="submit">Registrar</button>
</form>
);
}
Reutilizando Schemas
Schemas Zod são reutilizáveis. Você pode compor e estender:
const enderecoSchema = z.object({
rua: z.string().min(5),
cidade: z.string().min(3),
cep: z.string().regex(/^\d{5}-\d{3}$/, 'CEP inválido'),
});
const usuarioComEnderecoSchema = z.object({
nome: z.string().min(3),
endereco: enderecoSchema,
});
Otimizações e Boas Práticas
Validação em Tempo Real e Performance
React Hook Form valida por padrão apenas na submissão. Para feedback imediato, use mode:
const { register, watch, formState: { errors } } = useForm({
resolver: zodResolver(schema),
mode: 'onChange', // Valida enquanto digita
});
Aviso:
mode: 'onChange'valida em cada keystroke. Para formulários complexos, considereonBlurpara menos re-renders.
Campos Dinâmicos com useFieldArray
Para listas dinâmicas (múltiplos endereços, telefones), use useFieldArray:
import { useFieldArray } from 'react-hook-form';
const schema = z.object({
contatos: z.array(z.object({
telefone: z.string().regex(/^\d{10,11}$/, 'Telefone inválido'),
})),
});
export function ContatosDinamicos() {
const { register, control, handleSubmit } = useForm({
resolver: zodResolver(schema),
});
const { fields, append, remove } = useFieldArray({
control,
name: 'contatos',
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`contatos.${index}.telefone`)} />
<button type="button" onClick={() => remove(index)}>
Remover
</button>
</div>
))}
<button type="button" onClick={() => append({ telefone: '' })}>
Adicionar Contato
</button>
<button type="submit">Enviar</button>
</form>
);
}
Integração com API (Side Effects)
Para chamadas assíncronas após validação:
const onSubmit = async (data: UserFormData) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Erro no servidor');
alert('Usuário criado com sucesso');
} catch (error) {
console.error(error);
}
};
Conclusão
Você aprendeu que React Hook Form + Zod é a combinação ideal para formulários modernos: React Hook Form gerencia estado com eficiência, Zod valida com type safety. Dominar useForm, register, schemas aninhados e useFieldArray te coloca no nível profissional. Na prática, sempre reutilize schemas, escolha o mode correto de validação e teste schemas separadamente do componente.