Códigos & Helpers de Produção
Custom hooks, validadores, formatadores e utilitários prontos para copiar e colar no seu projeto real.
useDebounce Hook
Atrasa a atualização de um estado até que o usuário pare de digitar pelo intervalo definido. Ideal para campos de busca com autocompletion.
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delayMs: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delayMs);
return () => {
clearTimeout(handler);
};
}, [value, delayMs]);
return debouncedValue;
}useLocalStorage com Sync Entre Abas
Hook seguro para persistir estado no localStorage com parsing de JSON automático e listener para sincronizar quando outra aba atualizar o storage.
import { useState, useEffect, useCallback } from "react";
export function useLocalStorage<T>(key: string, initialValue: T): [T, (val: T | ((prev: T) => T)) => void] {
const readValue = useCallback((): T => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.warn(`Erro ao ler localStorage chave "${key}":`, error);
return initialValue;
}
}, [key, initialValue]);
const [storedValue, setStoredValue] = useState<T>(readValue);
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
if (typeof window === "undefined") return;
try {
const newValue = value instanceof Function ? value(storedValue) : value;
window.localStorage.setItem(key, JSON.stringify(newValue));
setStoredValue(newValue);
window.dispatchEvent(new StorageEvent("storage", { key, newValue: JSON.stringify(newValue) }));
} catch (error) {
console.warn(`Erro ao salvar localStorage chave "${key}":`, error);
}
},
[key, storedValue]
);
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
setStoredValue(JSON.parse(e.newValue) as T);
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, [key]);
return [storedValue, setValue];
}Validador e Formatador de CPF
Algoritmo oficial com checagem dos 2 dígitos verificadores e função de máscara para formatação (000.000.000-00).
export function isValidCPF(cpf: string): boolean {
const clean = cpf.replace(/\D/g, "");
if (clean.length !== 11) return false;
// Elimina CPFs invalidos com todos os numeros iguais (111.111.111-11, etc)
if (/^(\d)\1{10}$/.test(clean)) return false;
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += parseInt(clean.charAt(i), 10) * (10 - i);
}
let rev = 11 - (sum % 11);
if (rev === 10 || rev === 11) rev = 0;
if (rev !== parseInt(clean.charAt(9), 10)) return false;
sum = 0;
for (let i = 0; i < 10; i++) {
sum += parseInt(clean.charAt(i), 10) * (11 - i);
}
rev = 11 - (sum % 11);
if (rev === 10 || rev === 11) rev = 0;
if (rev !== parseInt(clean.charAt(10), 10)) return false;
return true;
}
export function maskCPF(cpf: string): string {
const clean = cpf.replace(/\D/g, "").slice(0, 11);
return clean
.replace(/^(\d{3})(\d)/, "$1.$2")
.replace(/^(\d{3})\.(\d{3})(\d)/, "$1.$2.$3")
.replace(/\.(\d{3})(\d)/, ".$1-$2");
}Formatador de Moeda BRL e Percentual
Formatação performática utilizando a API nativa Intl.NumberFormat sem dependências externas.
const brlCurrencyFormatter = new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export function formatBRL(amountInCentsOrNumber: number, isCents = false): string {
const val = isCents ? amountInCentsOrNumber / 100 : amountInCentsOrNumber;
return brlCurrencyFormatter.format(val);
}
const percentFormatter = new Intl.NumberFormat("pt-BR", {
style: "percent",
minimumFractionDigits: 1,
maximumFractionDigits: 2,
});
export function formatPercent(rate: number): string {
// rate: 0.15 => "15,0%"
return percentFormatter.format(rate);
}Gerador de Slugs Amigáveis (PT-BR)
Converte títulos em URLs limpas e otimizadas para SEO, tratando acentos (á, ç, õ), símbolos especiais e múltiplos hifens.
export function slugify(text: string): string {
return text
.toString()
.normalize("NFD") // Decompõe caracteres acentuados
.replace(/[\u0300-\u036f]/g, "") // Remove marcas diacríticas
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, "") // Remove caracteres especiais
.replace(/\s+/g, "-") // Troca espaços por hifens
.replace(/-+/g, "-"); // Remove múltiplos hifens consecutivos
}Tempo Relativo ("há 5 minutos") com Intl
Calcula e exibe quanto tempo passou desde uma data ('há 2 horas', 'há 3 dias', 'ontem') sem bibliotecas pesadas como momentjs.
export function formatRelativeTime(date: Date | string | number): string {
const rtf = new Intl.RelativeTimeFormat("pt-BR", { numeric: "auto" });
const timeMs = typeof date === "number" ? date : new Date(date).getTime();
const diffSeconds = Math.round((timeMs - Date.now()) / 1000);
const cutoffs = [
{ unit: "year", seconds: 31536000 },
{ unit: "month", seconds: 2592000 },
{ unit: "day", seconds: 86400 },
{ unit: "hour", seconds: 3600 },
{ unit: "minute", seconds: 60 },
{ unit: "second", seconds: 1 },
] as const;
for (const { unit, seconds } of cutoffs) {
if (Math.abs(diffSeconds) >= seconds || unit === "second") {
const value = Math.round(diffSeconds / seconds);
return rtf.format(value, unit);
}
}
return "agora mesmo";
}Setup de Conexão Neon Postgres + Drizzle ORM
Configuração singleton pronta para uso no Next.js App Router e Server Actions com pool HTTP serverless.
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not set in environment variables");
}
// Cria cliente SQL serverless com suporte a scale-to-zero
const sql = neon(process.env.DATABASE_URL);
// Exporta instância do Drizzle tipada com o schema
export const db = drizzle(sql, { schema });Rate Limiter em Memória para Rotas Next.js
Proteja endpoints de API contra spam e ataques de força bruta com um algoritmo simples de Sliding Window / Token Bucket em memória.
interface RateLimitTracker {
count: number;
resetAt: number;
}
const rateLimitMap = new Map<string, RateLimitTracker>();
export function checkRateLimit(
identifier: string,
limit: number = 60,
windowMs: number = 60000
): { allowed: boolean; remaining: number; resetInMs: number } {
const now = Date.now();
const entry = rateLimitMap.get(identifier);
if (!entry || now > entry.resetAt) {
rateLimitMap.set(identifier, {
count: 1,
resetAt: now + windowMs,
});
return { allowed: true, remaining: limit - 1, resetInMs: windowMs };
}
if (entry.count >= limit) {
return { allowed: false, remaining: 0, resetInMs: entry.resetAt - now };
}
entry.count += 1;
return { allowed: true, remaining: limit - entry.count, resetInMs: entry.resetAt - now };
}