<h2>Introdução: Por que React Hook Form com Zod?</h2>
<p>Trabalhar com formulários em React sempre foi um desafio. Historicamente, desenvolvedores enfrentavam duas abordagens: gerenciar estado manualmente com <code>useState</code> (verboso e propenso a bugs) ou utilizar bibliotecas pesadas que adicionavam muita complexidade ao bundle. React Hook Form surgiu para resolver esse problema com uma abordagem minimalista, baseada em uncontrolled components, que reduz re-renders desnecessários e mantém o código limpo.</p>
<p>A integração com Zod eleva essa experiência a outro nível. Zod é uma biblioteca de validação TypeScript-first que permite definir schemas de dados de forma declarativa e type-safe. Quando usamos React Hook Form com Zod, obtemos validação robusta, mensagens de erro automáticas e inferência de tipos completa — tudo isso sem perder a leveza e performance que React Hook Form oferece. Esta combinação é hoje considerada o padrão ouro para formulários modernos em React com TypeScript.</p>
<h2>Fundamentos: React Hook Form e Zod</h2>
<h3>O que é React Hook Form?</h3>
<p>React Hook Form é uma biblioteca que simplifica o gerenciamento de estado de formulários através de hooks customizados. Ao contrário de bibliotecas tradicionais que aplicam re-renders em todo o formulário sempre que um campo muda, React Hook Form utiliza uma estratégia inteligente: apenas os campos que realmente foram modificados sofrem re-renders. Isso é possível porque a biblioteca mantém referências aos elementos DOM e extrai valores sob demanda.</p>
<p>A filosofia central é: menos estado em memória, menos processamento, melhor performance. Você registra seus campos com <code>register()</code> e a biblioteca cuida do resto. Não há necessidade de criar handlers <code>onChange</code> individuais ou sincronizar estado externo — tudo é derivado dos valores reais dos inputs.</p>
<h3>O que é Zod e por que integrar?</h3>
<p>Zod é um parser e validador de schemas construído em TypeScript. Diferente de validadores genéricos como Joi ou Yup, Zod foi especificamente projetado para tirar proveito do sistema de tipos TypeScript, permitindo que você infira tipos automaticamente a partir de seus schemas. Uma única definição de schema gera tanto a validação em tempo de execução quanto os tipos TypeScript em tempo de compilação.</p>
<p>A integração com React Hook Form acontece através do resolver <code>zodResolver</code>, que transforma seu schema Zod em um validador que o hook form entende. Isso significa que suas regras de validação são expressas uma única vez e reutilizáveis em qualquer contexto — não apenas em React, mas em backend, workers, ou qualquer lugar onde você use TypeScript.</p>
<h2>Configuração Prática: Setup e Primeiro Formulário</h2>
<h3>Instalação e Configuração Inicial</h3>
<p>Começamos instalando as dependências necessárias. Em um projeto React com TypeScript, você precisará de:</p>
<pre><code class="language-bash">npm install react-hook-form zod @hookform/resolvers</code></pre>
<p>A biblioteca <code>@hookform/resolvers</code> é essencial — ela fornece os resolvers (funções adaptadoras) que permitem integrar Zod com React Hook Form.</p>
<p>Vamos criar um exemplo prático: um formulário de registro de usuário. Primeiro, definimos nosso schema Zod:</p>
<pre><code class="language-typescript">import { z } from 'zod';
const userSchema = z.object({
name: z.string()
.min(3, 'Nome deve ter no mínimo 3 caracteres')
.max(50, 'Nome não pode exceder 50 caracteres'),
email: z.string()
.email('Formato de email inválido'),
password: z.string()
.min(8, 'Senha deve ter no mínimo 8 caracteres')
.regex(/[A-Z]/, 'Senha deve conter ao menos uma letra maiúscula')
.regex(/[0-9]/, 'Senha deve conter ao menos um número'),
confirmPassword: z.string(),
age: z.number()
.int('Idade deve ser um número inteiro')
.min(18, 'Deve ser maior de idade'),
terms: z.boolean()
.refine(val => val === true, 'Você deve aceitar os termos')
});
// Refinement para validação cross-field
const userSchemaWithConfirm = userSchema.refine(
(data) => data.password === data.confirmPassword,
{
message: 'As senhas não conferem',
path: ['confirmPassword']
}
);
// Inferir tipo do schema
type UserFormData = z.infer<typeof userSchemaWithConfirm>;</code></pre>
<p>Este schema define todas as regras de validação de forma declarativa. O método <code>refine()</code> permite validações customizadas que envolvem múltiplos campos. Repare como usamos <code>z.infer</code> para gerar o tipo TypeScript automaticamente — não precisamos manter tipos e schema em sincronização manualmente.</p>
<h3>Construindo o Componente do Formulário</h3>
<p>Agora criamos o componente React que utiliza esse schema:</p>
<pre><code class="language-typescript">import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { userSchemaWithConfirm, type UserFormData } from './schema';
function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
watch
} = useForm<UserFormData>({
resolver: zodResolver(userSchemaWithConfirm),
mode: 'onBlur' // Valida quando o usuário sai do campo
});
const password = watch('password'); // Observa mudanças no campo password
async function onSubmit(data: UserFormData) {
// data já está totalmente tipado e validado
console.log('Dados validados:', data);
// Simula envio ao servidor
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Usuário registrado com sucesso!');
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-md mx-auto p-6">
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Nome</label>
<input
{...register('name')}
type="text"
className={`w-full px-3 py-2 border rounded ${
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Seu nome completo"
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Email</label>
<input
{...register('email')}
type="email"
className={`w-full px-3 py-2 border rounded ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="seu@email.com"
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Senha</label>
<input
{...register('password')}
type="password"
className={`w-full px-3 py-2 border rounded ${
errors.password ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Mínimo 8 caracteres"
/>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Confirmar Senha</label>
<input
{...register('confirmPassword')}
type="password"
className={`w-full px-3 py-2 border rounded ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Repita a senha"
/>
{errors.confirmPassword && (
<p className="text-red-500 text-sm mt-1">
{errors.confirmPassword.message}
</p>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Idade</label>
<input
{...register('age', { valueAsNumber: true })}
type="number"
className={`w-full px-3 py-2 border rounded ${
errors.age ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="18+"
/>
{errors.age && (
<p className="text-red-500 text-sm mt-1">{errors.age.message}</p>
)}
</div>
<div className="mb-6">
<label className="flex items-center">
<input
{...register('terms')}
type="checkbox"
className="w-4 h-4 mr-2"
/>
<span className="text-sm">
Aceito os termos e condições
</span>
</label>
{errors.terms && (
<p className="text-red-500 text-sm mt-1">{errors.terms.message}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700 disabled:bg-gray-400"
>
{isSubmitting ? 'Registrando...' : 'Registrar'}
</button>
</form>
);
}
export default RegistrationForm;</code></pre>
<p>Observe como o hook <code>useForm</code> é configurado com <code>zodResolver</code>. A propriedade <code>mode: 'onBlur'</code> define quando a validação ocorre — existem outras opções como <code>'onChange'</code> ou <code>'onSubmit'</code>. O <code>register()</code> conecta cada input ao formulário sem necessidade de state explícito. Os erros vêm direto do objeto <code>formState.errors</code> e já contêm as mensagens que definimos no schema.</p>
<h2>Padrões Avançados e Otimizações</h2>
<h3>Validação Condicional e Lógica Complexa</h3>
<p>Às vezes você precisa de validações que dependem de múltiplos campos ou de lógica mais sofisticada. Zod oferece ferramentas poderosas para isso:</p>
<pre><code class="language-typescript">const checkoutSchema = z.object({
paymentMethod: z.enum(['credit_card', 'bank_transfer', 'pix']),
cardNumber: z.string().optional(),
cardHolder: z.string().optional(),
cvv: z.string().optional(),
bankCode: z.string().optional(),
bankAccount: z.string().optional(),
pixKey: z.string().optional()
}).refine(
(data) => {
if (data.paymentMethod === 'credit_card') {
return data.cardNumber && data.cardHolder && data.cvv;
}
if (data.paymentMethod === 'bank_transfer') {
return data.bankCode && data.bankAccount;
}
if (data.paymentMethod === 'pix') {
return data.pixKey;
}
return false;
},
{
message: 'Preencha os dados do método de pagamento selecionado',
path: ['paymentMethod']
}
);
type CheckoutFormData = z.infer<typeof checkoutSchema>;</code></pre>
<p>Este exemplo demonstra como validar campos condicionais. A lógica <code>refine()</code> verifica qual método foi selecionado e garante que apenas os campos relevantes sejam preenchidos.</p>
<h3>Formulários Dinâmicos com useFieldArray</h3>
<p>React Hook Form oferece <code>useFieldArray</code> para trabalhar com listas de campos — perfeito para adicionar/remover itens dinamicamente:</p>
<pre><code class="language-typescript">import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const itemSchema = z.object({
name: z.string().min(1, 'Nome obrigatório'),
quantity: z.number().min(1, 'Quantidade deve ser pelo menos 1'),
price: z.number().min(0.01, 'Preço deve ser maior que zero')
});
const invoiceSchema = z.object({
clientName: z.string().min(1, 'Nome do cliente obrigatório'),
items: z.array(itemSchema).min(1, 'Adicione pelo menos um item')
});
type InvoiceFormData = z.infer<typeof invoiceSchema>;
function InvoiceForm() {
const {
control,
register,
handleSubmit,
formState: { errors }
} = useForm<InvoiceFormData>({
resolver: zodResolver(invoiceSchema),
defaultValues: {
clientName: '',
items: [{ name: '', quantity: 1, price: 0 }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items'
});
function onSubmit(data: InvoiceFormData) {
console.log('Fatura validada:', data);
const total = data.items.reduce((sum, item) => sum + (item.quantity * item.price), 0);
console.log('Total:', total);
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-2xl mx-auto p-6">
<div className="mb-6">
<label className="block text-sm font-medium mb-2">Cliente</label>
<input
{...register('clientName')}
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded"
placeholder="Nome do cliente"
/>
{errors.clientName && (
<p className="text-red-500 text-sm mt-1">{errors.clientName.message}</p>
)}
</div>
<fieldset className="mb-6">
<legend className="block text-sm font-medium mb-4">Itens</legend>
{fields.map((field, index) => (
<div key={field.id} className="grid grid-cols-4 gap-2 mb-3">
<input
{...register(items.${index}.name)}
type="text"
placeholder="Descrição"
className="px-3 py-2 border border-gray-300 rounded"
/>
<input
{...register(items.${index}.quantity, { valueAsNumber: true })}
type="number"
placeholder="Qtd"
className="px-3 py-2 border border-gray-300 rounded"
/>
<input
{...register(items.${index}.price, { valueAsNumber: true })}
type="number"
step="0.01"
placeholder="Preço"
className="px-3 py-2 border border-gray-300 rounded"
/>
<button
type="button"
onClick={() => remove(index)}
className="bg-red-600 text-white rounded hover:bg-red-700"
>
Remover
</button>
</div>
))}
{errors.items && (
<p className="text-red-500 text-sm">{errors.items.message}</p>
)}
<button
type="button"
onClick={() => append({ name: '', quantity: 1, price: 0 })}
className="mt-2 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Adicionar Item
</button>
</fieldset>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700"
>
Gerar Fatura
</button>
</form>
);
}
export default InvoiceForm;</code></pre>
<p>O <code>useFieldArray</code> gerencia arrays de campos de forma eficiente. Você registra campos usando notação de índice (<code>items.${index}.name</code>) e tem métodos para <code>append</code>, <code>remove</code> e <code>insert</code> itens.</p>
<h3>Integrando Validação Assíncrona</h3>
<p>Às vezes você precisa validar dados contra um servidor — por exemplo, verificar se um email já existe:</p>
<pre><code class="language-typescript">const userSchemaAsync = z.object({
username: z.string()
.min(3, 'Mínimo 3 caracteres')
.refine(
async (username) => {
const response = await fetch(/api/check-username?username=${username});
return response.ok; // true se disponível
},
{
message: 'Nome de usuário já existe'
}
),
email: z.string()
.email('Email inválido')
.refine(
async (email) => {
const response = await fetch(/api/check-email?email=${email});
return response.ok;
},
{
message: 'Email já cadastrado'
}
)
});
type UserAsyncFormData = z.infer<typeof userSchemaAsync>;
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isValidating }
} = useForm<UserAsyncFormData>({
resolver: zodResolver(userSchemaAsync),
mode: 'onBlur' // Validação assíncrona é melhor com onBlur
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<div className="mb-4">
<input
{...register('username')}
type="text"
placeholder="Escolha seu usuário"
className="w-full px-3 py-2 border border-gray-300 rounded"
/>
{isValidating && <p className="text-blue-500 text-sm">Verificando...</p>}
{errors.username && (
<p className="text-red-500 text-sm mt-1">{errors.username.message}</p>
)}
</div>
<div className="mb-4">
<input
{...register('email')}
type="email"
placeholder="Seu email"
className="w-full px-3 py-2 border border-gray-300 rounded"
/>
{isValidating && <p className="text-blue-500 text-sm">Verificando...</p>}
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded">
Registrar
</button>
</form>
);
}
export default SignupForm;</code></pre>
<p>O <code>refine()</code> do Zod oferece suporte nativo para validadores assíncronos. Quando você usa <code>mode: 'onBlur'</code>, o formulário aguarda as requisições assíncronas antes de considerar o campo validado.</p>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>React Hook Form com Zod representa a combinação moderna ideal para formulários em React com TypeScript</strong>: eficiência através de uncontrolled components, segurança de tipos com inferência automática, e validação poderosa sem sacrificar performance ou simplicidade. A integração é natural e ambas as bibliotecas compartilham uma filosofia de minimalismo e foco em resolver um problema bem.</p>
<p>O segundo ponto crítico é que <strong>schemas Zod são reutilizáveis além de React</strong> — você pode usar a mesma definição de validação em seu backend Node.js, em testes, em web workers, ou qualquer contexto TypeScript. Isso reduz duplicação de lógica e mantém suas regras de negócio centralizadas. Uma source of truth para validação é fundamental em arquiteturas escaláveis.</p>
<p>Por fim, <strong>os padrões avançados como validação condicional, campos dinâmicos e validação assíncrona</strong> mostram que essa stack não é apenas simples, mas também robusta o suficiente para aplicações complexas. Você não precisa procurar outras bibliotecas para resolver desafios reais de formulários — React Hook Form e Zod já oferecem os tools necessários.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react-hook-form.com/" target="_blank" rel="noopener noreferrer">React Hook Form - Documentação Oficial</a></li>
<li><a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Zod - GitHub e Documentação</a></li>
<li><a href="https://react-hook-form.com/get-started#Applyvalidation" target="_blank" rel="noopener noreferrer">React Hook Form + Zod Integration</a></li>
<li><a href="https://basarat.gitbook.io/typescript/" target="_blank" rel="noopener noreferrer">TypeScript Deep Dive - Type Inference</a></li>
<li><a href="https://kentcdodds.com/blog/please-stop-building-form-wrappers" target="_blank" rel="noopener noreferrer">Advanced React Patterns - Form Management</a></li>
</ul>
<p><!-- FIM --></p>