React & Frontend

Boas Práticas de Zod com React Hook Form: Schemas Complexos e Erros Customizados para Times Ágeis

21 min de leitura

Boas Práticas de Zod com React Hook Form: Schemas Complexos e Erros Customizados para Times Ágeis

Introdução: Por Que Zod com React Hook Form? A validação de formulários é um dos maiores desafios na construção de aplicações web modernas. Precisamos validar dados no cliente para melhorar a experiência do usuário, mas também manter mensagens de erro claras e contextualizadas. Quando você combina Zod (uma biblioteca TypeScript-first de schema validation) com React Hook Form (um gerenciador de estado de formulário performático), você consegue criar validações robustas sem sacrificar performance ou legibilidade do código. Neste artigo, vou guiá-lo além das validações básicas. Você vai aprender a construir schemas complexos com validações condicionais, refinar mensagens de erro para contextos específicos e criar uma experiência de usuário profissional. Este é o conhecimento que você precisa ter para trabalhar com formulários em produção. Fundamentos: Entendendo Zod e React Hook Form O Que é Zod? Zod é uma biblioteca de validação de schemas TypeScript que permite definir validações de forma declarativa. Diferente de outras soluções, o Zod oferece inferência de tipos automática

<h2>Introdução: Por Que Zod com React Hook Form?</h2>

<p>A validação de formulários é um dos maiores desafios na construção de aplicações web modernas. Precisamos validar dados no cliente para melhorar a experiência do usuário, mas também manter mensagens de erro claras e contextualizadas. Quando você combina <strong>Zod</strong> (uma biblioteca TypeScript-first de schema validation) com <strong>React Hook Form</strong> (um gerenciador de estado de formulário performático), você consegue criar validações robustas sem sacrificar performance ou legibilidade do código.</p>

<p>Neste artigo, vou guiá-lo além das validações básicas. Você vai aprender a construir schemas complexos com validações condicionais, refinar mensagens de erro para contextos específicos e criar uma experiência de usuário profissional. Este é o conhecimento que você precisa ter para trabalhar com formulários em produção.</p>

<h2>Fundamentos: Entendendo Zod e React Hook Form</h2>

<h3>O Que é Zod?</h3>

<p>Zod é uma biblioteca de validação de schemas TypeScript que permite definir validações de forma declarativa. Diferente de outras soluções, o Zod oferece inferência de tipos automática — quando você cria um schema, você ganha o tipo TypeScript correspondente gratuitamente. Isso elimina a duplicação de código entre validação e tipagem.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

// Define um schema simples

const usuarioSchema = z.object({

email: z.string().email(&#039;Email inválido&#039;),

idade: z.number().min(18, &#039;Deve ter pelo menos 18 anos&#039;),

nome: z.string().min(3, &#039;Nome deve ter no mínimo 3 caracteres&#039;),

});

// TypeScript infere automaticamente este tipo:

type Usuario = z.infer&lt;typeof usuarioSchema&gt;;

// Equivalente a: { email: string; idade: number; nome: string }</code></pre>

<h3>Integrando Zod com React Hook Form</h3>

<p>React Hook Form é extremamente leve e focado em performance. Quando integrado com Zod via <code>zodResolver</code>, o Hook Form executa a validação usando seu schema Zod e popula os erros automaticamente. A integração é simples, mas poderosa.</p>

<pre><code class="language-typescript">import { useForm } from &#039;react-hook-form&#039;;

import { zodResolver } from &#039;@hookform/resolvers/zod&#039;;

const MinhaForm = () =&gt; {

const {

register,

handleSubmit,

formState: { errors },

} = useForm({

resolver: zodResolver(usuarioSchema),

});

const onSubmit = (data) =&gt; console.log(data);

return (

&lt;form onSubmit={handleSubmit(onSubmit)}&gt;

&lt;input {...register(&#039;email&#039;)} placeholder=&quot;Email&quot; /&gt;

{errors.email &amp;&amp; &lt;p&gt;{errors.email.message}&lt;/p&gt;}

&lt;input {...register(&#039;idade&#039;, { valueAsNumber: true })} type=&quot;number&quot; /&gt;

{errors.idade &amp;&amp; &lt;p&gt;{errors.idade.message}&lt;/p&gt;}

&lt;button type=&quot;submit&quot;&gt;Enviar&lt;/button&gt;

&lt;/form&gt;

);

};</code></pre>

<h2>Schemas Complexos e Validações Avançadas</h2>

<h3>Validações Condicionais com <code>.refine()</code> e <code>.superRefine()</code></h3>

<p>Às vezes você precisa validar um campo baseado no valor de outro campo — por exemplo, confirmar que a senha de confirmação é igual à senha. Zod oferece <code>.refine()</code> para validações customizadas simples e <code>.superRefine()</code> para casos onde você precisa de controle total sobre os erros.</p>

<pre><code class="language-typescript">const senhaSchema = z.object({

senha: z.string().min(8, &#039;Senha deve ter no mínimo 8 caracteres&#039;),

confirmaSenha: z.string(),

}).refine(

(data) =&gt; data.senha === data.confirmaSenha,

{

message: &#039;As senhas devem ser iguais&#039;,

path: [&#039;confirmaSenha&#039;], // Define em qual campo o erro aparecerá

}

);

// Uso com React Hook Form

const form = useForm({

resolver: zodResolver(senhaSchema),

});</code></pre>

<p>Para validações mais complexas que envolvem múltiplos campos com mensagens diferentes, use <code>.superRefine()</code>:</p>

<pre><code class="language-typescript">const pagamentoSchema = z.object({

tipo: z.enum([&#039;cartao&#039;, &#039;boleto&#039;]),

numeroCartao: z.string().optional(),

agencia: z.string().optional(),

}).superRefine((data, ctx) =&gt; {

if (data.tipo === &#039;cartao&#039; &amp;&amp; !data.numeroCartao) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

path: [&#039;numeroCartao&#039;],

message: &#039;Número do cartão é obrigatório&#039;,

});

}

if (data.tipo === &#039;boleto&#039; &amp;&amp; !data.agencia) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

path: [&#039;agencia&#039;],

message: &#039;Agência é obrigatória para boleto&#039;,

});

}

});</code></pre>

<h3>Arrays e Objetos Aninhados</h3>

<p>Formulários reais frequentemente contêm listas de itens ou estruturas aninhadas. Zod trata isso elegantemente com <code>.array()</code> e schemas aninhados:</p>

<pre><code class="language-typescript">const formularioCidadeSchema = z.object({

cidade: z.string().min(1, &#039;Cidade obrigatória&#039;),

endereco: z.object({

rua: z.string().min(1, &#039;Rua obrigatória&#039;),

numero: z.number().positive(&#039;Número deve ser positivo&#039;),

complemento: z.string().optional(),

}),

contatos: z.array(

z.object({

tipo: z.enum([&#039;email&#039;, &#039;telefone&#039;]),

valor: z.string().min(1, &#039;Valor obrigatório&#039;),

})

).min(1, &#039;Adicione pelo menos um contato&#039;),

});

type FormularioCidade = z.infer&lt;typeof formularioCidadeSchema&gt;;</code></pre>

<p>Ao usar isso em React Hook Form com campos dinâmicos, você pode iterar sobre os campos:</p>

<pre><code class="language-typescript">import { useFieldArray, Controller } from &#039;react-hook-form&#039;;

const FormularioCidade = () =&gt; {

const {

register,

control,

handleSubmit,

formState: { errors },

} = useForm({

resolver: zodResolver(formularioCidadeSchema),

});

const { fields, append, remove } = useFieldArray({

control,

name: &#039;contatos&#039;,

});

return (

&lt;form onSubmit={handleSubmit((data) =&gt; console.log(data))}&gt;

&lt;input {...register(&#039;cidade&#039;)} placeholder=&quot;Cidade&quot; /&gt;

{errors.cidade &amp;&amp; &lt;p&gt;{errors.cidade.message}&lt;/p&gt;}

&lt;fieldset&gt;

&lt;legend&gt;Endereço&lt;/legend&gt;

&lt;input {...register(&#039;endereco.rua&#039;)} placeholder=&quot;Rua&quot; /&gt;

{errors.endereco?.rua &amp;&amp; &lt;p&gt;{errors.endereco.rua.message}&lt;/p&gt;}

&lt;input

{...register(&#039;endereco.numero&#039;, { valueAsNumber: true })}

type=&quot;number&quot;

/&gt;

{errors.endereco?.numero &amp;&amp; &lt;p&gt;{errors.endereco.numero.message}&lt;/p&gt;}

&lt;/fieldset&gt;

&lt;fieldset&gt;

&lt;legend&gt;Contatos&lt;/legend&gt;

{fields.map((field, index) =&gt; (

&lt;div key={field.id}&gt;

&lt;select {...register(contatos.${index}.tipo)}&gt;

&lt;option value=&quot;email&quot;&gt;Email&lt;/option&gt;

&lt;option value=&quot;telefone&quot;&gt;Telefone&lt;/option&gt;

&lt;/select&gt;

&lt;input

{...register(contatos.${index}.valor)}

placeholder=&quot;Valor&quot;

/&gt;

{errors.contatos?.[index]?.valor &amp;&amp; (

&lt;p&gt;{errors.contatos[index]?.valor?.message}&lt;/p&gt;

)}

&lt;button type=&quot;button&quot; onClick={() =&gt; remove(index)}&gt;

Remover

&lt;/button&gt;

&lt;/div&gt;

))}

&lt;button type=&quot;button&quot; onClick={() =&gt; append({ tipo: &#039;email&#039;, valor: &#039;&#039; })}&gt;

Adicionar Contato

&lt;/button&gt;

&lt;/fieldset&gt;

&lt;button type=&quot;submit&quot;&gt;Enviar&lt;/button&gt;

&lt;/form&gt;

);

};</code></pre>

<h2>Mensagens de Erro Customizadas</h2>

<h3>Localizando Mensagens (i18n)</h3>

<p>Em aplicações internacionais, você precisa oferecer mensagens de erro no idioma do usuário. Com Zod, você pode definir mensagens customizadas globalmente ou por schema:</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

// Definir mensagens globais para o português

const zodPt = z;

// Ou criar schemas com mensagens customizadas

const loginSchema = z.object({

email: z

.string({ required_error: &#039;Email é obrigatório&#039; })

.email(&#039;Insira um email válido&#039;),

senha: z

.string({ required_error: &#039;Senha é obrigatória&#039; })

.min(6, &#039;Senha deve ter no mínimo 6 caracteres&#039;)

.max(50, &#039;Senha não pode exceder 50 caracteres&#039;),

});

// Para suportar múltiplos idiomas, crie uma função factory:

const criarSchemaAutenticacao = (idioma: &#039;pt&#039; | &#039;en&#039;) =&gt; {

const mensagens = {

pt: {

emailObrigatorio: &#039;Email é obrigatório&#039;,

emailInvalido: &#039;Insira um email válido&#039;,

senhaObrigatoria: &#039;Senha é obrigatória&#039;,

senhaMinima: &#039;Senha deve ter no mínimo 6 caracteres&#039;,

},

en: {

emailObrigatorio: &#039;Email is required&#039;,

emailInvalido: &#039;Enter a valid email&#039;,

senhaObrigatoria: &#039;Password is required&#039;,

senhaMinima: &#039;Password must have at least 6 characters&#039;,

},

};

const msg = mensagens[idioma];

return z.object({

email: z

.string({ required_error: msg.emailObrigatorio })

.email(msg.emailInvalido),

senha: z

.string({ required_error: msg.senhaObrigatoria })

.min(6, msg.senhaMinima),

});

};</code></pre>

<h3>Exibindo Erros de Forma Amigável</h3>

<p>Não basta validar — você precisa comunicar os erros de forma clara. Crie componentes reutilizáveis que tratam erros com elegância:</p>

<pre><code class="language-typescript">import React from &#039;react&#039;;

import { FieldError } from &#039;react-hook-form&#039;;

interface InputComErroProps {

label: string;

erro?: FieldError;

registro: any;

tipo?: &#039;text&#039; | &#039;email&#039; | &#039;password&#039; | &#039;number&#039;;

}

const InputComErro: React.FC&lt;InputComErroProps&gt; = ({

label,

erro,

registro,

tipo = &#039;text&#039;,

}) =&gt; {

return (

&lt;div style={{ marginBottom: &#039;1rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039;, fontWeight: &#039;bold&#039; }}&gt;

{label}

&lt;/label&gt;

&lt;input

{...registro}

type={tipo}

style={{

width: &#039;100%&#039;,

padding: &#039;0.5rem&#039;,

borderRadius: &#039;4px&#039;,

border: 2px solid ${erro ? &#039;#dc2626&#039; : &#039;#e5e7eb&#039;},

fontSize: &#039;1rem&#039;,

fontFamily: &#039;inherit&#039;,

}}

/&gt;

{erro &amp;&amp; (

&lt;p style={{ color: &#039;#dc2626&#039;, marginTop: &#039;0.25rem&#039;, fontSize: &#039;0.875rem&#039; }}&gt;

{erro.message}

&lt;/p&gt;

)}

&lt;/div&gt;

);

};

// Uso em um formulário

const FormularioSimples = () =&gt; {

const {

register,

handleSubmit,

formState: { errors },

} = useForm({

resolver: zodResolver(loginSchema),

});

return (

&lt;form onSubmit={handleSubmit((data) =&gt; console.log(data))}&gt;

&lt;InputComErro

label=&quot;Email&quot;

erro={errors.email}

registro={register(&#039;email&#039;)}

tipo=&quot;email&quot;

/&gt;

&lt;InputComErro

label=&quot;Senha&quot;

erro={errors.senha}

registro={register(&#039;senha&#039;)}

tipo=&quot;password&quot;

/&gt;

&lt;button type=&quot;submit&quot;&gt;Entrar&lt;/button&gt;

&lt;/form&gt;

);

};</code></pre>

<h3>Erros a Nível de Formulário</h3>

<p>Às vezes você precisa validar todo o formulário — por exemplo, verificar se um usuário existe no servidor. Use o callback <code>onError</code> do <code>handleSubmit</code>:</p>

<pre><code class="language-typescript">const FormularioComValidacaoServidor = () =&gt; {

const [erroGlobal, setErroGlobal] = React.useState(&#039;&#039;);

const {

register,

handleSubmit,

formState: { errors },

} = useForm({

resolver: zodResolver(loginSchema),

});

const onSubmit = async (data) =&gt; {

try {

setErroGlobal(&#039;&#039;);

const response = await fetch(&#039;/api/login&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(data),

});

if (!response.ok) {

if (response.status === 401) {

setErroGlobal(&#039;Email ou senha inválidos&#039;);

} else {

setErroGlobal(&#039;Erro ao conectar. Tente novamente.&#039;);

}

} else {

console.log(&#039;Login bem-sucedido&#039;);

}

} catch (error) {

setErroGlobal(&#039;Erro de conexão&#039;);

}

};

return (

&lt;form onSubmit={handleSubmit(onSubmit)}&gt;

{erroGlobal &amp;&amp; (

&lt;div style={{ padding: &#039;1rem&#039;, background: &#039;#fee2e2&#039;, color: &#039;#991b1b&#039;, borderRadius: &#039;4px&#039;, marginBottom: &#039;1rem&#039; }}&gt;

{erroGlobal}

&lt;/div&gt;

)}

&lt;InputComErro

label=&quot;Email&quot;

erro={errors.email}

registro={register(&#039;email&#039;)}

tipo=&quot;email&quot;

/&gt;

&lt;InputComErro

label=&quot;Senha&quot;

erro={errors.senha}

registro={register(&#039;senha&#039;)}

tipo=&quot;password&quot;

/&gt;

&lt;button type=&quot;submit&quot;&gt;Entrar&lt;/button&gt;

&lt;/form&gt;

);

};</code></pre>

<h2>Exemplo Completo: Formulário de Cadastro Avançado</h2>

<p>Vamos unir tudo isso em um exemplo prático — um formulário de cadastro com validações complexas, campos dinâmicos e mensagens customizadas:</p>

<pre><code class="language-typescript">import React from &#039;react&#039;;

import { useForm, useFieldArray } from &#039;react-hook-form&#039;;

import { zodResolver } from &#039;@hookform/resolvers/zod&#039;;

import { z } from &#039;zod&#039;;

// Schema com validações avançadas

const cadastroSchema = z.object({

nome: z

.string()

.min(3, &#039;Nome deve ter no mínimo 3 caracteres&#039;)

.max(100, &#039;Nome não pode exceder 100 caracteres&#039;),

email: z

.string()

.email(&#039;Email inválido&#039;),

dataNascimento: z

.string()

.refine(

(data) =&gt; {

const idade = new Date().getFullYear() - new Date(data).getFullYear();

return idade &gt;= 18;

},

&#039;Você deve ter no mínimo 18 anos&#039;

),

plano: z.enum([&#039;gratuito&#039;, &#039;profissional&#039;, &#039;empresarial&#039;]),

cartao: z.object({

numero: z.string().regex(/^\d{16}$/, &#039;Cartão deve ter 16 dígitos&#039;),

mes: z.string().regex(/^\d{2}$/, &#039;Mês inválido&#039;),

ano: z.string().regex(/^\d{4}$/, &#039;Ano inválido&#039;),

cvv: z.string().regex(/^\d{3}$/, &#039;CVV deve ter 3 dígitos&#039;),

}).optional(),

interesses: z

.array(z.string())

.min(1, &#039;Selecione pelo menos um interesse&#039;),

termos: z

.boolean()

.refine((val) =&gt; val === true, &#039;Você deve aceitar os termos&#039;),

}).refine(

(data) =&gt; {

// Se o plano é pago, cartão é obrigatório

if ([&#039;profissional&#039;, &#039;empresarial&#039;].includes(data.plano)) {

return !!data.cartao?.numero;

}

return true;

},

{

message: &#039;Cartão de crédito é obrigatório para planos pagos&#039;,

path: [&#039;cartao&#039;],

}

);

type CadastroForm = z.infer&lt;typeof cadastroSchema&gt;;

const FormularioCadastro = () =&gt; {

const [enviado, setEnviado] = React.useState(false);

const {

register,

control,

watch,

handleSubmit,

formState: { errors },

} = useForm&lt;CadastroForm&gt;({

resolver: zodResolver(cadastroSchema),

defaultValues: {

interesses: [],

plano: &#039;gratuito&#039;,

},

});

const planoSelecionado = watch(&#039;plano&#039;);

const onSubmit = async (data: CadastroForm) =&gt; {

setEnviado(true);

console.log(&#039;Dados validados:&#039;, data);

// Enviar para servidor

};

return (

&lt;form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: &#039;600px&#039;, margin: &#039;0 auto&#039; }}&gt;

&lt;h2&gt;Cadastro&lt;/h2&gt;

{/ Nome /}

&lt;div style={{ marginBottom: &#039;1.5rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Nome&lt;/label&gt;

&lt;input

{...register(&#039;nome&#039;)}

style={{

width: &#039;100%&#039;,

padding: &#039;0.5rem&#039;,

border: 2px solid ${errors.nome ? &#039;#dc2626&#039; : &#039;#e5e7eb&#039;},

borderRadius: &#039;4px&#039;,

}}

/&gt;

{errors.nome &amp;&amp; &lt;p style={{ color: &#039;#dc2626&#039;, marginTop: &#039;0.25rem&#039; }}&gt;{errors.nome.message}&lt;/p&gt;}

&lt;/div&gt;

{/ Email /}

&lt;div style={{ marginBottom: &#039;1.5rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Email&lt;/label&gt;

&lt;input

{...register(&#039;email&#039;)}

type=&quot;email&quot;

style={{

width: &#039;100%&#039;,

padding: &#039;0.5rem&#039;,

border: 2px solid ${errors.email ? &#039;#dc2626&#039; : &#039;#e5e7eb&#039;},

borderRadius: &#039;4px&#039;,

}}

/&gt;

{errors.email &amp;&amp; &lt;p style={{ color: &#039;#dc2626&#039;, marginTop: &#039;0.25rem&#039; }}&gt;{errors.email.message}&lt;/p&gt;}

&lt;/div&gt;

{/ Data de Nascimento /}

&lt;div style={{ marginBottom: &#039;1.5rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Data de Nascimento&lt;/label&gt;

&lt;input

{...register(&#039;dataNascimento&#039;)}

type=&quot;date&quot;

style={{

width: &#039;100%&#039;,

padding: &#039;0.5rem&#039;,

border: 2px solid ${errors.dataNascimento ? &#039;#dc2626&#039; : &#039;#e5e7eb&#039;},

borderRadius: &#039;4px&#039;,

}}

/&gt;

{errors.dataNascimento &amp;&amp; &lt;p style={{ color: &#039;#dc2626&#039;, marginTop: &#039;0.25rem&#039; }}&gt;{errors.dataNascimento.message}&lt;/p&gt;}

&lt;/div&gt;

{/ Plano /}

&lt;div style={{ marginBottom: &#039;1.5rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Plano&lt;/label&gt;

&lt;select

{...register(&#039;plano&#039;)}

style={{

width: &#039;100%&#039;,

padding: &#039;0.5rem&#039;,

border: &#039;2px solid #e5e7eb&#039;,

borderRadius: &#039;4px&#039;,

}}

&gt;

&lt;option value=&quot;gratuito&quot;&gt;Gratuito&lt;/option&gt;

&lt;option value=&quot;profissional&quot;&gt;Profissional&lt;/option&gt;

&lt;option value=&quot;empresarial&quot;&gt;Empresarial&lt;/option&gt;

&lt;/select&gt;

&lt;/div&gt;

{/ Cartão (condicional) /}

{[&#039;profissional&#039;, &#039;empresarial&#039;].includes(planoSelecionado) &amp;&amp; (

&lt;div style={{ marginBottom: &#039;1.5rem&#039;, padding: &#039;1rem&#039;, background: &#039;#f3f4f6&#039;, borderRadius: &#039;4px&#039; }}&gt;

&lt;h4&gt;Informações do Cartão&lt;/h4&gt;

&lt;div style={{ marginBottom: &#039;1rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Número&lt;/label&gt;

&lt;input

{...register(&#039;cartao.numero&#039;)}

placeholder=&quot;1234567890123456&quot;

style={{

width: &#039;100%&#039;,

padding: &#039;0.5rem&#039;,

border: 2px solid ${errors.cartao?.numero ? &#039;#dc2626&#039; : &#039;#e5e7eb&#039;},

borderRadius: &#039;4px&#039;,

}}

/&gt;

{errors.cartao?.numero &amp;&amp; &lt;p style={{ color: &#039;#dc2626&#039; }}&gt;{errors.cartao.numero.message}&lt;/p&gt;}

&lt;/div&gt;

&lt;div style={{ display: &#039;grid&#039;, gridTemplateColumns: &#039;1fr 1fr 1fr&#039;, gap: &#039;0.5rem&#039; }}&gt;

&lt;div&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Mês&lt;/label&gt;

&lt;input {...register(&#039;cartao.mes&#039;)} placeholder=&quot;MM&quot; style={{ width: &#039;100%&#039;, padding: &#039;0.5rem&#039;, border: &#039;2px solid #e5e7eb&#039;, borderRadius: &#039;4px&#039; }} /&gt;

&lt;/div&gt;

&lt;div&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Ano&lt;/label&gt;

&lt;input {...register(&#039;cartao.ano&#039;)} placeholder=&quot;YYYY&quot; style={{ width: &#039;100%&#039;, padding: &#039;0.5rem&#039;, border: &#039;2px solid #e5e7eb&#039;, borderRadius: &#039;4px&#039; }} /&gt;

&lt;/div&gt;

&lt;div&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;CVV&lt;/label&gt;

&lt;input {...register(&#039;cartao.cvv&#039;)} placeholder=&quot;123&quot; style={{ width: &#039;100%&#039;, padding: &#039;0.5rem&#039;, border: &#039;2px solid #e5e7eb&#039;, borderRadius: &#039;4px&#039; }} /&gt;

&lt;/div&gt;

&lt;/div&gt;

&lt;/div&gt;

)}

{/ Interesses /}

&lt;div style={{ marginBottom: &#039;1.5rem&#039; }}&gt;

&lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;Interesses&lt;/label&gt;

&lt;div&gt;

{[&#039;Tecnologia&#039;, &#039;Negócios&#039;, &#039;Design&#039;, &#039;Marketing&#039;].map((interesse) =&gt; (

&lt;label key={interesse} style={{ display: &#039;flex&#039;, alignItems: &#039;center&#039;, marginBottom: &#039;0.5rem&#039; }}&gt;

&lt;input

type=&quot;checkbox&quot;

value={interesse}

{...register(&#039;interesses&#039;)}

style={{ marginRight: &#039;0.5rem&#039; }}

/&gt;

{interesse}

&lt;/label&gt;

))}

&lt;/div&gt;

{errors.interesses &amp;&amp; &lt;p style={{ color: &#039;#dc2626&#039; }}&gt;{errors.interesses.message}&lt;/p&gt;}

&lt;/div&gt;

{/ Termos /}

&lt;div style={{ marginBottom: &#039;1.5rem&#039; }}&gt;

&lt;label style={{ display: &#039;flex&#039;, alignItems: &#039;center&#039; }}&gt;

&lt;input

type=&quot;checkbox&quot;

{...register(&#039;termos&#039;)}

style={{ marginRight: &#039;0.5rem&#039; }}

/&gt;

Aceito os termos e condições

&lt;/label&gt;

{errors.termos &amp;&amp; &lt;p style={{ color: &#039;#dc2626&#039; }}&gt;{errors.termos.message}&lt;/p&gt;}

&lt;/div&gt;

&lt;button

type=&quot;submit&quot;

style={{

width: &#039;100%&#039;,

padding: &#039;0.75rem&#039;,

background: &#039;#3b82f6&#039;,

color: &#039;white&#039;,

border: &#039;none&#039;,

borderRadius: &#039;4px&#039;,

fontWeight: &#039;bold&#039;,

cursor: &#039;pointer&#039;,

}}

&gt;

Cadastrar

&lt;/button&gt;

{enviado &amp;&amp; &lt;p style={{ marginTop: &#039;1rem&#039;, color: &#039;#16a34a&#039; }}&gt;Cadastro realizado com sucesso!&lt;/p&gt;}

&lt;/form&gt;

);

};

export default FormularioCadastro;</code></pre>

<h2>Conclusão</h2>

<p>Ao dominar Zod com React Hook Form, você alcança três ganhos práticos na sua carreira de desenvolvedor:</p>

<ol>

<li><strong>Tipagem Segura e Validação Unificada</strong>: Ao usar <code>z.infer&lt;typeof schema&gt;</code>, você elimina a duplicação entre tipos TypeScript e validações runtime. Seu código fica mais mantível porque a &quot;fonte da verdade&quot; é única — o schema Zod.</li>

</ol>

<ol>

<li><strong>Controle Total Sobre Mensagens de Erro</strong>: Através de <code>.refine()</code>, <code>.superRefine()</code> e schemas condicionais, você pode criar experiências de erro contextualizadas. Usuários entendem exatamente o que fazer, reduzindo fricção no preenchimento de formulários.</li>

</ol>

<ol>

<li><strong>Escalabilidade em Formulários Complexos</strong>: Estruturas aninhadas, campos dinâmicos e validações cruzadas não são mais desafiadoras. A combinação Zod + Hook Form oferece abstrações que crescem com a complexidade da sua aplicação sem virar &quot;spaghetti code&quot;.</li>

</ol>

<h2>Referências</h2>

<ul>

<li><a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Documentação Oficial do Zod</a></li>

<li><a href="https://react-hook-form.com/form-builder" target="_blank" rel="noopener noreferrer">React Hook Form com Zod Integration</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/type-inference.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Type Inference</a></li>

<li><a href="https://blog.logrocket.com/build-strongly-typed-forms-react-zod/" target="_blank" rel="noopener noreferrer">Build Forms with Zod - LogRocket</a></li>

<li><a href="https://www.smashingmagazine.com/2022/09/inline-validation-web-forms-html5-javascript/" target="_blank" rel="noopener noreferrer">Advanced React Hook Form Patterns</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática
useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática

O Problema Real do useContext em Aplicações Escaláveis Quando iniciamos com R...

O que Todo Dev Deve Saber sobre Next.js App Router Avançado: Server Actions, Cache e Revalidação
O que Todo Dev Deve Saber sobre Next.js App Router Avançado: Server Actions, Cache e Revalidação

Server Actions: Fundamentos e Implementação Server Actions são funções execut...

Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática
Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática

O Que São Componentes Controlados e Não Controlados? Antes de mais nada, prec...