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.