Formulários Complexos em React: React Hook Form e Zod Validation na Prática Já leu

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 O pacote é essencial — ele adapta Zod ao padrão esperado por React Hook Form. Exemplo Básico de Formulário O método vincula campos ao formulário, enquanto valida antes de chamar . Os erros vêm pré-formatados do schema Zod.

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, considere onBlur para 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.

Referências


Artigos relacionados