<h2>Por Que React Hook Form e Zod Juntos?</h2>
<p>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. <strong>React Hook Form</strong> resolve o gerenciamento de estado com performance excepcional, mantendo o formulário desacoplado do componente. <strong>Zod</strong> é 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.</p>
<p>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.</p>
<h2>Configuração Inicial e Integração</h2>
<h3>Instalação das Dependências</h3>
<pre><code class="language-bash">npm install react-hook-form zod @hookform/resolvers</code></pre>
<p>O pacote <code>@hookform/resolvers</code> é essencial — ele adapta Zod ao padrão esperado por React Hook Form.</p>
<h3>Exemplo Básico de Formulário</h3>
<pre><code class="language-jsx">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>
);
}</code></pre>
<p>O método <code>register</code> vincula campos ao formulário, enquanto <code>handleSubmit</code> valida antes de chamar <code>onSubmit</code>. Os erros vêm pré-formatados do schema Zod.</p>
<h2>Validações Avançadas com Zod</h2>
<h3>Validações Condicionais e Refinamentos</h3>
<p>Formulários reais exigem lógica além de tipos básicos. Zod oferece <code>.refine()</code> e <code>.superRefine()</code> para isso:</p>
<pre><code class="language-jsx">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>
);
}</code></pre>
<h3>Reutilizando Schemas</h3>
<p>Schemas Zod são reutilizáveis. Você pode compor e estender:</p>
<pre><code class="language-jsx">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,
});</code></pre>
<h2>Otimizações e Boas Práticas</h2>
<h3>Validação em Tempo Real e Performance</h3>
<p>React Hook Form valida por padrão apenas na submissão. Para feedback imediato, use <code>mode</code>:</p>
<pre><code class="language-jsx">const { register, watch, formState: { errors } } = useForm({
resolver: zodResolver(schema),
mode: 'onChange', // Valida enquanto digita
});</code></pre>
<blockquote><p><strong>Aviso</strong>: <code>mode: 'onChange'</code> valida em cada keystroke. Para formulários complexos, considere <code>onBlur</code> para menos re-renders.</p></blockquote>
<h3>Campos Dinâmicos com <code>useFieldArray</code></h3>
<p>Para listas dinâmicas (múltiplos endereços, telefones), use <code>useFieldArray</code>:</p>
<pre><code class="language-jsx">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>
);
}</code></pre>
<h3>Integração com API (Side Effects)</h3>
<p>Para chamadas assíncronas após validação:</p>
<pre><code class="language-jsx">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);
}
};</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>React Hook Form + Zod</strong> é a combinação ideal para formulários modernos: React Hook Form gerencia estado com eficiência, Zod valida com type safety. Dominar <code>useForm</code>, <code>register</code>, schemas aninhados e <code>useFieldArray</code> te coloca no nível profissional. Na prática, sempre reutilize schemas, escolha o <code>mode</code> correto de validação e teste schemas separadamente do componente.</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://www.npmjs.com/package/@hookform/resolvers" target="_blank" rel="noopener noreferrer">@hookform/resolvers - npm</a></li>
<li><a href="https://www.smashingmagazine.com/2022/09/inline-validation-web-forms-constraint-validation-api/" target="_blank" rel="noopener noreferrer">Building Better Forms in React with React Hook Form</a></li>
<li><a href="https://www.totaltypescript.com/tutorials/zod" target="_blank" rel="noopener noreferrer">TypeScript-First Schema Validation with Zod - Tutorial</a></li>
</ul>