useMemo e useCallback no React: quando realmente preciso
1. Visão geral
useMemo e useCallback costumam ser usados por intuição: ou você envolve tudo “para ter certeza”, ou nunca usa. Ambas abordagens estão erradas. Este artigo explica o mecanismo por trás dos dois hooks, aponta três situações onde eles realmente são úteis e casos onde você deve pulá-los. Você também escreverá um teste para comprovar que a memoização funciona.
2. O problema fundamental: cada renderização cria tudo de novo
Toda vez que um component renderiza novamente, todo o corpo da função executa do início. Cada objeto, array e função declarados dentro são criados novos.
function Checkout({ customer }) {
const shipping = { city: customer.city, express: true };
const handlePay = () => console.log('Pagar para', customer.name);
return <PaymentForm shipping={shipping} onPay={handlePay} />;
}
Após cada renderização de Checkout, shipping e handlePay são novas referências, mesmo que o conteúdo não tenha mudado. JavaScript compara objetos e funções por referência:
{ city: 'Huế' } === { city: 'Huế' } // false
(() => 1) === (() => 1) // false
Normalmente isso é inofensivo, pois criar objetos é muito barato. Só se torna um problema quando algo compara referências. Pode ser React.memo, o array de dependências de useEffect, ou outro hook. useMemo e useCallback existem para manter referências estáveis entre renderizações.
3. useMemo: memorizar um valor
useMemo(calculate, deps) chama calculate na primeira renderização e salva o resultado. Nas renderizações seguintes, React apenas recalcula quando um elemento em deps muda, comparado usando Object.is.
3.1. Evitar cálculos caros a cada renderização
import { useMemo, useState } from 'react';
function OrderReport({ orders }) {
const [keyword, setKeyword] = useState('');
const [darkMode, setDarkMode] = useState(false);
const visibleOrders = useMemo(() => {
const kw = keyword.toLowerCase();
return orders
.filter((o) => o.customer.toLowerCase().includes(kw))
.sort((a, b) => b.total - a.total);
}, [orders, keyword]);
return (
<div className={darkMode ? 'dark' : ''}>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={() => setDarkMode((d) => !d)}>Mudar tema</button>
<ul>
{visibleOrders.map((o) => (
<li key={o.id}>{o.customer}: {o.total}</li>
))}
</ul>
</div>
);
}
Explicação:
- As operações
filteresortapenas executam novamente quandoordersoukeywordmudam. - Quando você clica “Mudar tema”,
darkModemuda e o component renderiza novamente. Neste momento,useMemoretorna o array memorizado, não filtra novamente. sorté seguro aqui porquefilterjá retornou um novo array. Nenhuma operação modifica diretamente o proporders.
Como saber se um cálculo é “caro”? Meça em vez de adivinhar:
console.time('filtrar pedidos');
const result = orders.filter((o) => o.customer.includes(keyword));
console.timeEnd('filtrar pedidos');
filtrar pedidos: 0.08ms
Um ponto prático: se um cálculo leva menos de cerca de 1ms na máquina de desenvolvimento, quase nunca precisa de memo. Lembre-se de medir em build de produção, e melhor ainda, ative throttling de CPU no DevTools, pois máquinas de usuários geralmente são mais lentas que a sua.
3.2. Manter um objeto estável para passar adiante
O segundo caso não está relacionado à velocidade do cálculo. Aqui você precisa de um objeto com referência estável para servir como dependency:
function CustomerOrders({ customerId, status }) {
const [orders, setOrders] = useState([]);
const query = useMemo(() => ({ customerId, status }), [customerId, status]);
useEffect(() => {
fetchOrders(query).then(setOrders);
}, [query]);
return <OrderTable orders={orders} />;
}
Se você remover useMemo, query será um novo objeto após cada renderização. Então o effect executará novamente continuamente, e como setOrders causa outra renderização, você pode entrar em um loop de chamadas de API infinitas.
Nota: Neste exemplo há uma maneira mais simples. Você cria o objeto dentro do effect e coloca os valores primitivos na dependency:
useEffect(() => { fetchOrders({ customerId, status }).then(setOrders); }, [customerId, status]);. Prefira essa abordagem. Apenas useuseMemoquando o objeto precisar viver fora do effect, por exemplo quando é passado para um component filho.
4. useCallback: memorizar uma função
useCallback(fn, deps) é equivalente a useMemo(() => fn, deps). Retorna a mesma função que você passou e mantém a referência estável até deps mudar. useCallback não torna a função mais rápida. Este hook só faz sentido quando a função é comparada em algum lugar.
4.1. Combinado com React.memo
React.memo ignora a renderização de um component filho se as props são iguais à renderização anterior (comparação rasa). Se uma prop é uma função criada novo a cada vez, memo perde seu efeito.
import { memo, useCallback, useState } from 'react';
const OrderRow = memo(function OrderRow({ order, onSelect, onRender }) {
onRender?.(order.id);
return <li onClick={() => onSelect(order.id)}>{order.customer}</li>;
});
export function OrderList({ orders, onRender }) {
const [selectedId, setSelectedId] = useState(null);
const [note, setNote] = useState('');
const handleSelect = useCallback((id) => setSelectedId(id), []);
return (
<>
<input aria-label="nota" value={note} onChange={(e) => setNote(e.target.value)} />
<ul>
{orders.map((o) => (
<OrderRow key={o.id} order={o} onSelect={handleSelect} onRender={onRender} />
))}
</ul>
<p>Selecionado: {selectedId ?? 'nenhum'}</p>
</>
);
}
Explicação:
handleSelecttem dependency vazia porquesetSelectedIdé sempre estável (React garante isso para setter deuseState).- Quando você digita na caixa de notas,
OrderListrenderiza novamente. No entanto, cadaOrderRowrecebe o mesmoorder, o mesmohandleSelect, o mesmoonRender, entãomemoos ignora. - Se você mudar
handleSelectpara uma função seta comum, todas as linhas renderizarão novamente a cada digitação.
Ponto-chave para lembrar: useCallback sem memo no lado receptor é praticamente inútil. Passar um callback memorizado para um elemento DOM <button> não economiza nada.
4.2. Função usada como dependency
function useOrderPolling(customerId) {
const [orders, setOrders] = useState([]);
const refresh = useCallback(async () => {
const data = await fetchOrders({ customerId });
setOrders(data);
}, [customerId]);
useEffect(() => {
refresh();
const id = setInterval(refresh, 10_000);
return () => clearInterval(id);
}, [refresh]);
return { orders, refresh };
}
refresh é usado tanto dentro do effect quanto retornado para o component externo. Graças a useCallback, o interval é criado apenas quando customerId muda. Qualquer component que use refresh como dependency também não rederiza desnecessariamente.
Esta é uma regra para seguir ao escrever custom hooks: funções retornadas para fora devem ser envolvidas com useCallback. Você não sabe antecipadamente se quem usar o hook colocará na dependency ou passará para memo.
5. Verificar com testes
Em vez de confiar que memo “provavelmente funciona”, você pode escrever testes para isso. Instale Vitest e React Testing Library:
npm install -D vitest jsdom @testing-library/react
Configure o ambiente jsdom em vite.config.js:
export default {
test: { environment: 'jsdom' },
};
Teste para OrderList da seção 4.1:
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { OrderList } from './OrderList';
const orders = [
{ id: 1, customer: 'Lan' },
{ id: 2, customer: 'Minh' },
];
describe('OrderList', () => {
it('não renderiza OrderRow novamente ao apenas digitar nota', () => {
const onRender = vi.fn();
render(<OrderList orders={orders} onRender={onRender} />);
expect(onRender).toHaveBeenCalledTimes(2);
fireEvent.change(screen.getByLabelText('nota'), { target: { value: 'envio rápido' } });
expect(onRender).toHaveBeenCalledTimes(2);
});
it('ainda atualiza ao selecionar um pedido', () => {
render(<OrderList orders={orders} />);
fireEvent.click(screen.getByText('Minh'));
expect(screen.getByText('Selecionado: 2')).toBeTruthy();
});
});
✓ OrderList > não renderiza OrderRow novamente ao apenas digitar nota
✓ OrderList > ainda atualiza ao selecionar um pedido
Tente remover useCallback e execute novamente. O primeiro teste falhará com expected 4 calls, received 2… ou melhor, receberá 4 chamadas em vez de 2. Essa é evidência clara de que o hook está funcionando. Declare onRender fora e passe a mesma referência, caso contrário este prop em si quebrará memo.
Além de testes, você deve usar a aba Profiler do React DevTools com a opção “Highlight updates” para ver visualmente qual component renderiza novamente.
6. Quando NÃO usar
Memoização não é gratuita. React precisa armazenar o valor antigo, armazenar o array de dependências e compará-los a cada renderização. O código também fica mais difícil de ler. Você deve pular nos seguintes casos:
- Cálculo barato: somar alguns números, concatenar strings, fazer
mapem dezenas de itens. - Valor primitivo:
useMemo(() => a + b, [a, b])retorna um número. Números já são comparados por valor, então memo não ajuda. - Callback passado para elemento DOM ou para component filho sem
memo. - Component filho ainda recebe prop que muda a cada vez. Por exemplo,
<OrderRow order={{ ...o }} />ou passarchildrencomo JSX. JSX também é um novo objeto a cada renderização, entãomemosempre verá diferença. - Possível refatorar a estrutura em vez de memoizar. Mover state mais perto de onde é usado, ou passar components pesados através de
children, geralmente elimina renderizações desnecessárias muito melhor.
Nota: Não confie em
useMemopara garantir lógica correta. React tem direito de limpar o cache, por exemplo em alguns casos com Suspense ou no futuro. Seu código deve funcionar corretamente mesmo sem memo. Este hook é apenas uma otimização de desempenho. Da mesma forma, não coloque side effects como chamadas de API ou escrita emlocalStoragedentro deuseMemo. Seu lugar é emuseEffectou event handlers.
Erro mais comum é dependência faltando, levando a closures desatualizados:
const handleSave = useCallback(() => {
saveOrder({ ...order, note });
}, [order]); // falta `note`
handleSave sempre usará o valor de note da primeira criação. Ative a regra react-hooks/exhaustive-deps do eslint-plugin-react-hooks e não desabilite seus avisos.
7. React Compiler muda o quê
React Compiler, estável desde final de 2025, analisa components no build e insere automaticamente memoização em nível mais granular do que você normalmente escreveria manualmente. Para tentar no seu projeto Vite ou Babel:
npm install -D babel-plugin-react-compiler
Com o compiler ativado, você praticamente não precisa escrever useMemo, useCallback ou memo. Os hooks antigos continuam funcionando. São úteis quando você precisa controlar exatamente uma dependência de um effect. O compiler pode apenas otimizar código seguindo as regras do React: components puros, sem modificar props ou state diretamente. Portanto, entender estes dois hooks neste artigo ainda é essencial para ler código antigo e fazer debug quando o compiler pula um component.
8. Conclusão
useMemo e useCallback resolvem o mesmo problema: manter referências estáveis entre renderizações. Eles realmente valem usar em três situações:
- Um cálculo que você mediu é caro.
- O valor ou função é passado para um component envolvido em
React.memo. - O valor ou função está na dependency de
useEffectou outro hook, incluindo funções retornadas de custom hooks.
Fora desses casos, mantenha o código simples. Meça com Profiler, prove com testes, e considere ativar React Compiler para deixar a ferramenta fazer esse trabalho para você.