O Gerador De CPF O Gerador De CPF

Máscara de CPF em React: Componente Controlado sem Dependências

Componente React para aplicar máscara de CPF (XXX.XXX.XXX-XX) em tempo real usando input controlado. Sem bibliotecas de máscara, apenas useState e useRef. Abordagem diferente da máscara com JavaScript puro porque o React controla o valor do input via state.

Função de formatação

function formatCpf(value: string): string {
  const digits = value.replace(/\D/g, "").slice(0, 11);
  if (digits.length <= 3) return digits;
  if (digits.length <= 6) return digits.slice(0, 3) + "." + digits.slice(3);
  if (digits.length <= 9)
    return digits.slice(0, 3) + "." + digits.slice(3, 6) + "." + digits.slice(6);
  return (
    digits.slice(0, 3) +
    "." +
    digits.slice(3, 6) +
    "." +
    digits.slice(6, 9) +
    "-" +
    digits.slice(9)
  );
}

Componente

import { useState, useRef, useCallback } from "react";

function CpfInput({ value, onChange, ...props }: {
  value: string;
  onChange: (cpf: string) => void;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange">) {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const input = e.target;
      const cursorPos = input.selectionStart ?? 0;
      const prevLen = value.length;
      const formatted = formatCpf(input.value);
      const diff = formatted.length - prevLen;

      onChange(formatted);

      // Corrige posição do cursor após formatação
      requestAnimationFrame(() => {
        const newPos = cursorPos + diff;
        input.setSelectionRange(newPos, newPos);
      });
    },
    [value, onChange]
  );

  return (
    <input
      ref={inputRef}
      type="text"
      inputMode="numeric"
      placeholder="000.000.000-00"
      maxLength={14}
      value={value}
      onChange={handleChange}
      {...props}
    />
  );
}

Análise do código

formatCpf é a mesma lógica da versão vanilla: remove não-dígitos, limita a 11 caracteres e insere separadores progressivamente.

No React, o <input> é controlado: o valor vem do state via value, não do DOM. Isso cria um problema: ao formatar, o React re-renderiza o input e o cursor vai para o final. A solução usa requestAnimationFrame para reposicionar o cursor após o React atualizar o DOM.

inputMode="numeric" abre o teclado numérico no mobile sem restringir o type a number (que não aceita pontos e traço).

Uso

function Form() {
  const [cpf, setCpf] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const digits = cpf.replace(/\D/g, "");
    console.log("CPF (apenas dígitos):", digits);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="cpf">CPF</label>
      <CpfInput id="cpf" value={cpf} onChange={setCpf} />
      <button type="submit">Enviar</button>
    </form>
  );
}

O componente aceita todas as props de <input> via spread (...props), permitindo adicionar className, id, aria-label e qualquer outro atributo.

Com validação

Para combinar máscara com validação em tempo real:

function CpfField() {
  const [cpf, setCpf] = useState("");
  const [error, setError] = useState("");

  function isValidCpf(value: string): boolean {
    const digits = value.replace(/\D/g, "");
    if (digits.length !== 11) return false;
    if (/^(\d)\1{10}$/.test(digits)) return false;

    for (let t = 9; t < 11; t++) {
      let sum = 0;
      for (let c = 0; c < t; c++) {
        sum += Number(digits[c]) * ((t + 1) - c);
      }
      if (Number(digits[t]) !== ((10 * sum) % 11) % 10) return false;
    }
    return true;
  }

  const handleBlur = () => {
    const digits = cpf.replace(/\D/g, "");
    if (digits.length === 0) {
      setError("");
    } else if (!isValidCpf(cpf)) {
      setError("CPF inválido");
    } else {
      setError("");
    }
  };

  return (
    <div>
      <CpfInput value={cpf} onChange={setCpf} onBlur={handleBlur} />
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

A validação é disparada no onBlur (quando o campo perde foco), evitando mensagens de erro enquanto o usuário ainda está digitando.

Testes

Testes com Vitest e Testing Library:

import { describe, it, expect } from "vitest";

describe("formatCpf", () => {
  it("retorna vazio para string sem dígitos", () => {
    expect(formatCpf("")).toBe("");
    expect(formatCpf("abc")).toBe("");
  });

  it("não formata até 3 dígitos", () => {
    expect(formatCpf("529")).toBe("529");
  });

  it("insere primeiro ponto após 3 dígitos", () => {
    expect(formatCpf("5299")).toBe("529.9");
    expect(formatCpf("529982")).toBe("529.982");
  });

  it("insere segundo ponto após 6 dígitos", () => {
    expect(formatCpf("5299822")).toBe("529.982.2");
  });

  it("insere traço após 9 dígitos", () => {
    expect(formatCpf("52998224725")).toBe("529.982.247-25");
  });

  it("limita a 11 dígitos", () => {
    expect(formatCpf("529982247251234")).toBe("529.982.247-25");
  });

  it("remove caracteres não numéricos", () => {
    expect(formatCpf("529.982.247-25")).toBe("529.982.247-25");
  });
});

Precisa de CPFs para testar o componente? O gerador de CPF cria números válidos com ou sem formatação.

Veja também: máscara de CPF com JavaScript puro e validar CPF em JavaScript.