kb.erickguedes.com
React: Construindo Interfaces Modernas

Formulários e Validação

Aula 5 de 8

Formulários Complexos

Controlled vs Uncontrolled na prática

// Controlled — React gerencia cada input
function FormControlled() {
  const [form, setForm] = useState({
    nome: "",
    email: "",
    idade: "",
  });

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  }

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    console.log(form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="nome" value={form.nome} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="idade" value={form.idade} onChange={handleChange} />
      <button type="submit">Enviar</button>
    </form>
  );
}

React Hook Form

npm install react-hook-form

Básico

import { useForm, type SubmitHandler } from "react-hook-form";

interface FormData {
  nome: string;
  email: string;
  idade: number;
}

function MeuForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>();

  const onSubmit: SubmitHandler<FormData> = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("nome", { required: "Nome é obrigatório" })} />
      {errors.nome && <span>{errors.nome.message}</span>}

      <input {...register("email", {
        required: "Email é obrigatório",
        pattern: {
          value: /^\S+@\S+$/i,
          message: "Email inválido",
        },
      })} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="number" {...register("idade", {
        min: { value: 18, message: "Idade mínima: 18" },
      })} />
      {errors.idade && <span>{errors.idade.message}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Enviando..." : "Enviar"}
      </button>
    </form>
  );
}

Controller (inputs customizados)

import { Controller } from "react-hook-form";

interface FormData {
  data: string;
}

function FormComData() {
  const { control, handleSubmit } = useForm<FormData>();

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        name="data"
        control={control}
        rules={{ required: "Data é obrigatória" }}
        render={({ field, fieldState }) => (
          <div>
            <input type="date" {...field} />
            {fieldState.error && <span>{fieldState.error.message}</span>}
          </div>
        )}
      />
      <button type="submit">Enviar</button>
    </form>
  );
}

watch — Observar Mudanças

function FormComWatch() {
  const { register, watch } = useForm<{ senha: string; confirmar: string }>();

  const senha = watch("senha");
  const confirmar = watch("confirmar");

  return (
    <form>
      <input type="password" {...register("senha")} />
      <input type="password" {...register("confirmar")} />

      {confirmar && senha !== confirmar && (
        <span>Senhas não conferem</span>
      )}

      {/* watch também pode observar múltiplos campos */}
      {watch(["senha", "confirmar"]).every(Boolean) && (
        <p>Todos os campos preenchidos</p>
      )}
    </form>
  );
}

Validação com Zod

npm install zod @hookform/resolvers
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const schema = z.object({
  nome: z.string().min(2, "Mínimo 2 caracteres"),
  email: z.string().email("Email inválido"),
  idade: z.coerce.number().int().positive().min(18, "Mínimo 18 anos"),
  cargo: z.enum(["dev", "design", "pm"], {
    errorMap: () => ({ message: "Selecione um cargo válido" }),
  }),
  termos: z.literal(true, {
    errorMap: () => ({ message: "Aceite os termos" }),
  }),
});

type FormSchema = z.infer<typeof schema>;

function FormComZod() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormSchema>({
    resolver: zodResolver(schema),
    defaultValues: {
      nome: "",
      email: "",
      idade: undefined,
      cargo: undefined,
      termos: false,
    },
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("nome")} />
      {errors.nome && <span>{errors.nome.message}</span>}

      <input {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="number" {...register("idade")} />
      {errors.idade && <span>{errors.idade.message}</span>}

      <select {...register("cargo")}>
        <option value="">Selecione...</option>
        <option value="dev">Desenvolvedor</option>
        <option value="design">Designer</option>
        <option value="pm">Product Manager</option>
      </select>
      {errors.cargo && <span>{errors.cargo.message}</span>}

      <label>
        <input type="checkbox" {...register("termos")} />
        Aceito os termos
      </label>
      {errors.termos && <span>{errors.termos.message}</span>}

      <button type="submit">Cadastrar</button>
    </form>
  );
}

Validação Assíncrona

const schema = z.object({
  email: z.string().email().refine(
    async (email) => {
      const res = await fetch(`/api/verificar-email?email=${email}`);
      return (await res.json()).disponivel;
    },
    { message: "Email já cadastrado" }
  ),
});

// Ou com useForm + validate customizado
function FormEmail() {
  const { register, handleSubmit, setError } = useForm();

  const onSubmit = async (data: { email: string }) => {
    const res = await fetch(`/api/verificar-email?email=${data.email}`);
    const result = await res.json();

    if (!result.disponivel) {
      setError("email", { message: "Email já cadastrado" });
      return;
    }

    console.log("Email disponível!");
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} />
      <button type="submit">Verificar</button>
    </form>
  );
}

Campos Dinâmicos (useFieldArray)

import { useFieldArray } from "react-hook-form";

interface FormComItens {
  itens: { nome: string; quantidade: number }[];
}

function ListaDinamica() {
  const { control, register, handleSubmit } = useForm<FormComItens>({
    defaultValues: { itens: [{ nome: "", quantidade: 1 }] },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: "itens",
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input
            {...register(`itens.${index}.nome`)}
            placeholder="Nome do item"
          />
          <input
            type="number"
            {...register(`itens.${index}.quantidade`)}
          />
          <button type="button" onClick={() => remove(index)}>Remover</button>
        </div>
      ))}

      <button type="button" onClick={() => append({ nome: "", quantidade: 1 })}>
        Adicionar Item
      </button>

      <button type="submit">Salvar</button>
    </form>
  );
}

Lab: Cadastro Completo

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

const cadastroSchema = z.object({
  nome: z.string().min(3, "Mínimo 3 caracteres"),
  email: z.string().email("Email inválido"),
  senha: z.string().min(6, "Mínimo 6 caracteres"),
  confirmarSenha: z.string(),
  dataNascimento: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Formato inválido"),
}).refine((data) => data.senha === data.confirmarSenha, {
  message: "Senhas não conferem",
  path: ["confirmarSenha"],
});

type CadastroSchema = z.infer<typeof cadastroSchema>;

function CadastroForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<CadastroSchema>({
    resolver: zodResolver(cadastroSchema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}
      style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
      <div>
        <label>Nome</label>
        <input {...register("nome")} />
        {errors.nome && <span>{errors.nome.message}</span>}
      </div>

      <div>
        <label>Email</label>
        <input type="email" {...register("email")} />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <label>Senha</label>
        <input type="password" {...register("senha")} />
        {errors.senha && <span>{errors.senha.message}</span>}
      </div>

      <div>
        <label>Confirmar Senha</label>
        <input type="password" {...register("confirmarSenha")} />
        {errors.confirmarSenha && <span>{errors.confirmarSenha.message}</span>}
      </div>

      <div>
        <label>Data de Nascimento</label>
        <input type="date" {...register("dataNascimento")} />
        {errors.dataNascimento && <span>{errors.dataNascimento.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Cadastrando..." : "Cadastrar"}
      </button>
    </form>
  );
}
npm install react-hook-form zod @hookform/resolvers

React Hook Form é a biblioteca padrão para formulários em React. Com o resolver do Zod, você tem tipagem + validação em um schema único, sem duplicação.