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 filter e sort apenas executam novamente quando orders ou keyword mudam.
  • Quando você clica “Mudar tema”, darkMode muda e o component renderiza novamente. Neste momento, useMemo retorna o array memorizado, não filtra novamente.
  • sort é seguro aqui porque filter já retornou um novo array. Nenhuma operação modifica diretamente o prop orders.

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 use useMemo quando 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:

  • handleSelect tem dependency vazia porque setSelectedId é sempre estável (React garante isso para setter de useState).
  • Quando você digita na caixa de notas, OrderList renderiza novamente. No entanto, cada OrderRow recebe o mesmo order, o mesmo handleSelect, o mesmo onRender, então memo os ignora.
  • Se você mudar handleSelect para 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 map em 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 passar children como JSX. JSX também é um novo objeto a cada renderização, então memo sempre 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 useMemo para 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 em localStorage dentro de useMemo. Seu lugar é em useEffect ou 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:

  1. Um cálculo que você mediu é caro.
  2. O valor ou função é passado para um component envolvido em React.memo.
  3. O valor ou função está na dependency de useEffect ou 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ê.

reacthooksusememousecallbackperformance