O Gerador De CPF O Gerador De CPF

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.