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.