Formulários com React Hook Form + Zod

lukesilva

👨‍💻 Lucas Silva

Posted on November 7, 2024

Formulários com React Hook Form + Zod

Introdução

Quando comecei a programar, precisava escrever bastante código em JavaScript puro para coletar dados de um formulário. De lá para cá, o desenvolvimento web evoluiu tanto que hoje temos bibliotecas que abstraem grande parte desse trabalho, facilitando a gestão dos dados dos formulários.

Neste artigo, vou mostrar como utilizar o React Hook Form para trabalhar com dados de formulários e zod para validar esses dados de forma eficiente e organizada.

Tô com pressa, me dê o código completo

Tá na mão:

import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Gamepad, Gamepad2 } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast, Toaster } from 'sonner';

const signUpForm = z
  .object({
    name: z
      .string()
      .min(2, { message: 'Nome deve ter ao menos 2 caracteres' })
      .max(50, { message: 'Nome deve ter no máximo 50 caracteres' }),
    email: z.string().email().max(100, { message: 'E-mail deve ter no máximo 100 caracteres' }),
    password: z.string().max(100, { message: 'Senha deve ter no máximo 100 caracteres' }),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: 'Senhas não conferem!',
    path: ['confirm'],
  });

type SignUpForm = z.infer<typeof signUpForm>;

export function App() {
  const {
    handleSubmit,
    register,
    reset,
    formState: { isSubmitting, errors },
  } = useForm<SignUpForm>({
    resolver: zodResolver(signUpForm),
  });

  async function handleSignup(data: SignUpForm) {
    console.log(data);
    await new Promise((resolve) => {
      setTimeout(resolve, 2000);
    });
    reset();
    toast.success('Conta criada com sucesso!');
  }

  return (
    <>
      <Toaster richColors />
      <div className="min-h-screen lg:grid lg:grid-cols-2 antialiased gap-8">
        <div className="hidden lg:flex h-full justify-center border-r border-foreground/5 bg-foreground text-muted-foreground items-center gap-3 text-lg">
          <Gamepad2 />
          <span className="font-semibold">gamers.shop</span>
        </div>
        <div className="flex flex-col items-center justify-center gap-6 min-h-screen">
          <div className="px-10 w-96 h-full flex flex-col justify-center items-center lg:w-[500px]">
            <h1 className="flex items-center gap-2 mb-4 text-2xl font-semibold tracking-tight">
              <Gamepad />
              Crie sua conta
              <Gamepad />
            </h1>
            <form onSubmit={handleSubmit(handleSignup)} className="space-y-4 w-full ">
              <div className="space-y-2">
                <Label htmlFor="name">Nome</Label>
                <Input id="name" type="text" {...register('name')} />
                {errors.name && <span className="text-sm text-red-500">{errors.name.message}</span>}
                {}
              </div>
              <div className="space-y-2">
                <Label htmlFor="email">Email</Label>
                <Input id="email" type="email" {...register('email')} />
                {errors.email && <span className="text-sm text-red-500">{errors.email.message}</span>}
              </div>
              <div className="space-y-2">
                <Label htmlFor="password">Senha</Label>
                <Input id="password" type="password" {...register('password')} />
                {errors.password && <span className="text-sm text-red-500">{errors.password.message}</span>}
              </div>
              <div className="space-y-2">
                <Label htmlFor="confirm">Confirme a senha</Label>
                <Input id="confirm" type="password" {...register('confirm')} />
                {errors.confirm && <span className="text-sm text-red-500">{errors.confirm.message}</span>}
              </div>

              <Button className="w-full" type="submit" disabled={isSubmitting}>
                Criar conta
              </Button>
            </form>
          </div>
        </div>
      </div>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Tutorial

Estruturando o Formulário

Primeiro, criei um formulário com quatro campos: nome, e-mail, senha e confirmação de senha. Para facilitar o desenvolvimento da interface, utilizei shadcn, tailwind e lucide-react. O uso de classes CSS pode parecer um pouco detalhado, mas elas ajudam a manter um design consistente.

<form className="space-y-4 w-full ">
  <div className="space-y-2">
    <Label htmlFor="name">Nome</Label>
    <Input id="name" type="text" placeholder="Digite o seu nome"/>
  </div>
  <div className="space-y-2">
    <Label htmlFor="email">Email</Label>
    <Input id="email" type="email" placeholder="Digite o seu e-mail"/>
  </div>
  <div className="space-y-2">
    <Label htmlFor="password">Senha</Label>
    <Input id="password" type="password" placeholder="Digite uma senha"/>
  </div>
  <div className="space-y-2">
    <Label htmlFor="confirm">Confirme a senha</Label>
    <Input id="confirm" type="password" placeholder="Digite a senha novamente"/>
  </div>

  <Button className="w-full" type="submit">
    Criar conta
  </Button>
</form>
Enter fullscreen mode Exit fullscreen mode

Criei 4 campos nesse form: nome, e-mail, password e confirm. Preciso validá-los de alguma forma. Como esse exemplo server para explicar o uso de zod, evitei utilizar as propriedades nativas do HTML (required, maxlength etc).

Validação de Dados com Zod

Para garantir que os dados inseridos nos campos estão corretos, criei um schema de validação com o zod. O schema define as restrições para cada campo e personaliza as mensagens de erro:

import { z } from 'zod';

const signUpFormSchema = z
  .object({
    name: z
      .string()
      .min(2, { message: 'Nome deve ter ao menos 2 caracteres' })
      .max(50, { message: 'Nome deve ter no máximo 50 caracteres' }),
    email: z.string().email().max(100, { message: 'E-mail deve ter no máximo 100 caracteres' }),
    password: z.string().max(100, { message: 'Senha deve ter no máximo 100 caracteres' }),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: 'Senhas não conferem!',
    path: ['confirm'],
  });

Enter fullscreen mode Exit fullscreen mode

Esse schema define os tipos e as validações necessárias para cada campo. O método .refine() foi utilizado para garantir que as senhas digitadas nos campos "password" e "confirm" são iguais.

.refine((data) => data.password === data.confirm, {
  message: 'Senhas não conferem!',
  path: ['confirm'],
});
Enter fullscreen mode Exit fullscreen mode

Integração com React Hook Form e Zod Resolver

O React Hook Form é uma biblioteca de formulários que melhora o desempenho ao reduzir re-renderizações desnecessárias e simplificar a manipulação de dados. Usei o useForm() para configurar o formulário, passando o schema de validação por meio do zodResolver:

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';

type SignUpForm = z.infer<typeof signUpFormSchema>;

const {
    handleSubmit,
    register,
    reset,
    formState: { isSubmitting, errors },
  } = useForm<SignUpForm>({
    resolver: zodResolver(signUpFormSchema),
  });
Enter fullscreen mode Exit fullscreen mode

As funções e variáveis que utilizei de useForm() são:

  • handleSubmit: lida com o envio do formulário.
  • register: vincula campos do formulário ao gerenciamento de dados do React Hook Form.
  • reset: redefine o formulário.
  • isSubmitting: indica se o formulário está sendo enviado.
  • errors: armazena erros de validação de cada campo. ### Função de Envio do Formulário Para simular o envio do formulário, criei a função handleSignup, que adiciona um tempo de espera para visualizar o estado isSubmitting e exibe uma mensagem de sucesso com o toast:
async function handleSignup(data: SignUpForm) {
  console.log(data);
  await new Promise((resolve) => {
    setTimeout(resolve, 2000);
  });
  reset();
  toast.success('Conta criada com sucesso!');
}
Enter fullscreen mode Exit fullscreen mode

Depois adicionei a função ao form:

<form onSubmit={handleSubmit(handleSignup)} className="space-y-4 w-full">
Enter fullscreen mode Exit fullscreen mode

E por fim, utilizei o register para denominar qual campo pertence a qual propriedade, e mostrei os erros (caso eles existam):

<div className="space-y-2">
  <Label htmlFor="name">Nome</Label>
  <Input id="name" type="text" {...register('name')} placeholder="Digite o seu nome" />
  {errors.name && <span className="text-sm text-red-500">{errors.name.message}</span>}
</div>
Enter fullscreen mode Exit fullscreen mode

O resultado ficou assim:

Tela de criação de conta feita neste tutorial

Considerações Finais

Nesse texto, mostrei uma forma simples de integrar React Hook Form e zod para validação de formulários não controlados. A biblioteca também funciona com componentes controlados, então consulte a documentação para explorar mais opções.

💖 💪 🙅 🚩
lukesilva
👨‍💻 Lucas Silva

Posted on November 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related