Artigo
Pare de adicionar o useCallback em toda função. Ele só ajuda em dois casos.
useCallback memoiza a referência da função, não o trabalho dela. Na maior parte do tempo isso não faz nada — exceto em dois casos específicos onde a identidade da referência importa de verdade.
- Publicado em
- 7 min de leitura
- 1 visualizações
Tem um hábito que dev React pega cedo: adicionar useCallback em toda callback, "só por garantia". Parece um ganho de performance — você viu num tutorial, o linter às vezes empurra nessa direção, e o nome da função até soa como se devesse ajudar.
Geralmente não ajuda. Na maior parte do tempo é um no-op com burocracia extra. useCallback só vale o preço em duas situações específicas — fora delas, você está pagando overhead sem benefício observável.
O que useCallback de fato faz
useCallback(fn, deps) devolve a mesma referência de função entre renders enquanto deps não mudarem. Só isso. O corpo da função ainda roda toda vez que ela é chamada. Nada é cacheado, nada é pulado — só a identidade do valor da função fica estável.
Essa distinção importa mais do que a documentação deixa claro. Olha isso:
function Counter({ initial }: { initial: number }) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
return <button onClick={increment}>{count}</button>;
}Isso é mais rápido que a versão sem useCallback? Não. O <button> não liga se increment é a mesma referência do render anterior — handlers de eventos DOM são reanexados de qualquer jeito. Você adicionou uma chamada de hook, uma comparação de dependency array e um slot de registro pra zero mudança observável.
A estabilidade da referência só importa quando alguma coisa downstream está lendo essa referência e reagindo a ela. Essa é a regra. Duas coisas no React fazem exatamente isso.
Caso 1: A função é dep de useEffect
Um aviso antes do exemplo: em 2026, data fetching quase nunca mora mais em useEffect. Mora em route loaders (TanStack Start, Remix), Server Components (Next.js App Router) ou hooks de bibliotecas como React Query e SWR que cuidam do lifecycle da requisição pra você. O framework ou o loader roda o fetch no servidor, entrega os dados resolvidos pro seu componente como prop, e seu código cliente só lê — sem useEffect, sem fetch, sem dep array pra esquecer.
O que continua morando em useEffect: subscriptions pra coisas fora do data model do React — DOM events, mensagens de WebSocket, entries de IntersectionObserver, updates de stores de bibliotecas de state não-React. Funções passadas pra dentro desses effects ainda causam re-run quando a referência muda. É aí que o problema do useCallback aparece hoje.
Versão real: um componente de chat recebe um handler onMessage do pai e registra ele numa conexão WebSocket.
type Props = {
roomId: string;
onMessage: (event: MessageEvent) => void;
};
function ChatRoom({ roomId, onMessage }: Props) {
useEffect(() => {
const ws = new WebSocket(`wss://example.com/rooms/${roomId}`);
ws.addEventListener("message", onMessage);
return () => ws.close();
}, [roomId, onMessage]);
// onMessage é uma ref nova a cada render do pai → effect desmonta e
// re-abre a WebSocket a cada render → conversa perde mensagens, queima
// handshakes de reconexão, e a sala nunca parece real-time
}A correção mora no pai — envolve onMessage com useCallback pra que o effect do filho só re-rode quando roomId realmente mudar:
function ChatPage({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const handleMessage = useCallback((event: MessageEvent) => {
const payload = JSON.parse(event.data) as Message;
setMessages((prev) => [...prev, payload]);
}, []);
return <ChatRoom roomId={roomId} onMessage={handleMessage} />;
}Mesma regra de antes: se a callback é local do filho e só é usada dentro do effect, define ela dentro do effect e pula o useCallback. Vá de useCallback só quando a função cruza fronteira de componente.
Caso 2: A função é passada para um filho React.memo
React.memo curto-circuita o render de um filho se todas as props comparam iguais à última vez (Object.is por padrão). Funções comparam por referência. Uma função nova a cada render do pai significa que o memo sempre desiste — o filho renderiza mesmo assim, e você ainda pagou o custo da comparação de prop por cima.
type Item = { id: number; label: string };
type Props = { items: Item[]; onSelect: (id: number) => void };
const ItemList = React.memo(function ItemList({ items, onSelect }: Props) {
return (
<ul>
{items.map((i) => (
<li key={i.id}>
<button onClick={() => onSelect(i.id)}>{i.label}</button>
</li>
))}
</ul>
);
});
function Page() {
const [items, setItems] = useState<Item[]>([]);
const handleSelect = (id: number) => console.log("picked", id);
return <ItemList items={items} onSelect={handleSelect} />;
}handleSelect é uma referência nova a cada render de Page. React.memo vê a prop mudar, desiste, re-renderiza a lista. O memo foi inútil.
function Page() {
const [items, setItems] = useState<Item[]>([]);
const handleSelect = useCallback((id: number) => console.log("picked", id), []);
return <ItemList items={items} onSelect={handleSelect} />;
}Agora a referência de handleSelect é estável, o memo segura, o filho pula o render quando nada mais mudou.
Esse caso é o motivo mais comum pra usar useCallback e o que mais gente esquece: envolver um filho com React.memo sem estabilizar as props de função dele é fazer metade do serviço.
O custo escala. Numa tabela virtualizada ou num data grid onde 50–500 linhas estão montadas ao mesmo tempo e cada linha é memoizada, uma prop onSelect ou onEdit sem estabilizar força toda linha visível a re-renderizar a cada mudança de state do pai — digitar num input de filtro, marcar um checkbox sem relação, qualquer coisa. É aí que dá pra sentir a diferença de verdade.
O custo que você paga fora desses casos
useCallback não é de graça. Toda chamada:
- Registra um slot de hook, contribuindo pro ledger interno de hooks do componente
- Roda uma comparação
Object.isem cada dependency a cada render - Mantém o closure da função anterior em memória até as deps mudarem
Pra componentes pequenos e que re-renderizam com frequência, a burocracia pode exceder o custo de deixar a função ser recriada. Recriar um function literal no V8 moderno é barato; comparar dependency arrays e gerenciar hook state também não é grátis.
Adicione por necessidade, não por reflexo. Os dois casos acima são o critério.
Um auto-check rápido
Antes de escrever useCallback, pergunta: alguma coisa downstream depende da identidade da referência dessa função?
- Tá num dependency array de
useEffect? Talvez. - É prop de um filho
React.memo? Talvez. - É prop de um filho normal / de um elemento DOM / usada só dentro do componente? Não.
Se a resposta é não, useCallback é peso morto.
O modelo mental é uma linha só: useCallback memoiza a referência, não o trabalho por trás dela. Use só quando a identidade da referência é o que outra parte do React está lendo. Fora desses casos, você não está otimizando nada e está adicionando ruído.
Update · 22/05/2026 — React 19 e o React Compiler
React 19 trouxe o React Compiler para o canal estável. O compilador analisa seu componente estaticamente, descobre onde memoização de fato ajuda e auto-memoiza as referências por baixo dos panos — useCallback, useMemo e React.memo viram coisas que você não escreve mais à mão pros casos comuns.
Implicações práticas:
- Código novo no compilador: escreve as funções normalmente. Deixa o compilador decidir o que estabilizar.
useCallbackescrito à mão vira code smell, a menos que você tenha um motivo específico que o compilador não consegue raciocinar (escape hatches, refs pra APIs imperativas, etc.). - Codebases ainda sem o compilador: as regras desse post continuam valendo. Os dois casos — deps de
useEffecte props deReact.memo— são exatamente os casos que o compilador tá tentando resolver pra você. Até você adotar, você é o compilador. - Código misto: o compilador é opt-in por arquivo via
"use memo"e respeita memoização manual existente. Migração aos poucos é suportada.
O modelo mental é o mesmo de antes — identidade de referência só importa quando alguma coisa downstream lê. O compilador só remove a etapa de anotação manual.