Validar CPF em React: Hook Customizado com Formulário
Hook customizado para validar CPF em React usando o algoritmo de módulo 11. Diferente da máscara de CPF em React, que formata a entrada, este artigo foca na validação com feedback de erro e integração com bibliotecas de formulário.
Função de validação
function isValidCpf(value: string): boolean {
const cpf = value.replace(/\D/g, "");
if (cpf.length !== 11) return false;
if (/^(\d)\1{10}$/.test(cpf)) return false;
for (let t = 9; t < 11; t++) {
let sum = 0;
for (let c = 0; c < t; c++) {
sum += Number(cpf[c]) * (t + 1 - c);
}
if (Number(cpf[t]) !== ((10 * sum) % 11) % 10) return false;
}
return true;
}Mesma lógica da implementação JavaScript pura, extraída como função para reutilizar nos exemplos abaixo.
Hook customizado
import { useState, useCallback } from "react";
interface UseCpfValidation {
error: string;
validate: (value: string) => boolean;
reset: () => void;
}
function useCpfValidation(): UseCpfValidation {
const [error, setError] = useState("");
const validate = useCallback((value: string): boolean => {
const digits = value.replace(/\D/g, "");
if (digits.length === 0) {
setError("CPF é obrigatório");
return false;
}
if (!isValidCpf(digits)) {
setError("CPF inválido");
return false;
}
setError("");
return true;
}, []);
const reset = useCallback(() => setError(""), []);
return { error, validate, reset };
}Análise do código
useCpfValidation encapsula o estado de erro e a lógica de validação em um hook reutilizável. validate retorna boolean para que o formulário decida se prossegue ou não, e atualiza o error internamente. reset limpa o erro, útil ao focar novamente no campo.
O useCallback evita recriações desnecessárias da função a cada render, mantendo referências estáveis para React.memo e listas de dependências de useEffect.
Formulário com validação
function CpfForm() {
const [cpf, setCpf] = useState("");
const { error, validate, reset } = useCpfValidation();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!validate(cpf)) return;
const digits = cpf.replace(/\D/g, "");
console.log("CPF válido:", digits);
};
return (
<form onSubmit={handleSubmit} noValidate>
<label htmlFor="cpf">CPF</label>
<input
id="cpf"
type="text"
inputMode="numeric"
placeholder="000.000.000-00"
maxLength={14}
value={cpf}
onChange={(e) => {
setCpf(e.target.value);
if (error) reset();
}}
onBlur={() => {
if (cpf.replace(/\D/g, "").length > 0) validate(cpf);
}}
aria-describedby={error ? "cpf-error" : undefined}
aria-invalid={!!error}
/>
{error && (
<p id="cpf-error" role="alert" style={{ color: "red" }}>
{error}
</p>
)}
<button type="submit">Enviar</button>
</form>
);
}O onBlur valida quando o usuário sai do campo, evitando mensagens de erro enquanto ainda digita. O onChange limpa o erro quando o usuário começa a corrigir. aria-invalid e aria-describedby conectam o campo ao erro para leitores de tela.
Com React Hook Form
Para projetos que usam React Hook Form, a validação encaixa no validate do register:
import { useForm } from "react-hook-form";
interface FormData {
cpf: string;
nome: string;
}
function CpfFormRHF() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit = (data: FormData) => {
const digits = data.cpf.replace(/\D/g, "");
console.log("CPF válido:", digits);
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<label htmlFor="cpf">CPF</label>
<input
id="cpf"
type="text"
inputMode="numeric"
placeholder="000.000.000-00"
maxLength={14}
{...register("cpf", {
required: "CPF é obrigatório",
validate: (value) => isValidCpf(value) || "CPF inválido",
})}
aria-invalid={!!errors.cpf}
/>
{errors.cpf && (
<p role="alert" style={{ color: "red" }}>
{errors.cpf.message}
</p>
)}
<button type="submit">Enviar</button>
</form>
);
}register com validate delega a lógica para isValidCpf. O React Hook Form gerencia estado, re-renders e submissão, e a função de validação é a mesma.
Testes
Testes com Vitest e Testing Library:
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
describe("isValidCpf", () => {
it("retorna true para CPF válido", () => {
expect(isValidCpf("529.982.247-25")).toBe(true);
});
it("retorna true para CPF sem máscara", () => {
expect(isValidCpf("52998224725")).toBe(true);
});
it("retorna false para dígitos repetidos", () => {
expect(isValidCpf("111.111.111-11")).toBe(false);
});
it("retorna false para dígito verificador incorreto", () => {
expect(isValidCpf("529.982.247-26")).toBe(false);
});
it("retorna false para tamanho incorreto", () => {
expect(isValidCpf("123.456")).toBe(false);
});
it("valida CPF com zeros à esquerda", () => {
expect(isValidCpf("000.000.001-91")).toBe(true);
});
});
describe("CpfForm", () => {
it("exibe erro ao submeter campo vazio", async () => {
const user = userEvent.setup();
render(<CpfForm />);
await user.click(screen.getByRole("button", { name: /enviar/i }));
expect(screen.getByRole("alert")).toHaveTextContent("CPF é obrigatório");
});
it("exibe erro para CPF inválido no blur", async () => {
const user = userEvent.setup();
render(<CpfForm />);
const input = screen.getByLabelText(/cpf/i);
await user.type(input, "12345678900");
await user.tab();
expect(screen.getByRole("alert")).toHaveTextContent("CPF inválido");
});
it("limpa erro ao digitar após erro", async () => {
const user = userEvent.setup();
render(<CpfForm />);
await user.click(screen.getByRole("button", { name: /enviar/i }));
expect(screen.getByRole("alert")).toBeInTheDocument();
await user.type(screen.getByLabelText(/cpf/i), "5");
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
it("submete formulário com CPF válido", async () => {
const user = userEvent.setup();
render(<CpfForm />);
await user.type(screen.getByLabelText(/cpf/i), "529.982.247-25");
await user.click(screen.getByRole("button", { name: /enviar/i }));
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
});Os testes do componente usam userEvent para simular interações reais (digitação, tab, clique) em vez de manipular o DOM diretamente. O getByRole("alert") encontra o erro via acessibilidade, garantindo que leitores de tela também o detectam.
Precisa de CPFs válidos para testar? Use o gerador de CPF para criar números de teste.
Veja também: máscara de CPF em React e validar CPF em JavaScript.