useMemo parece simples quando você lê a documentação. "Memoriza valores caros, otimiza performance, done!"

Aí você adiciona em alguns lugares e... nada muda. Ou pior, seu app fica mais lento. Você começa a se perguntar se está fazendo algo errado ou se useMemo é só mais um hook que todo mundo fala mas ninguém explica direito.

Passei meses usando useMemo de forma errada. A documentação do React explica O QUE ele faz, mas não explica QUANDO usar, POR QUE às vezes não funciona, e como evitar os erros que todo mundo comete.

Aqui está a explicação clara que eu queria quando comecei.


Por Que useMemo Parece Tão Confuso

O problema não é você. É que useMemo tem nuances invisíveis que a documentação não explica direito:

  1. Array de dependências funciona diferente do que você espera

  2. Nem tudo deve ser memoizado (às vezes piora performance)

  3. Comparação de objetos não funciona como primitivos

  4. Timing de execução pode te surpreender

A maioria dos tutoriais mostra exemplos básicos e pula essas partes importantes. Resultado: você decora sintaxe sem entender quando realmente precisa usar.

Vamos consertar isso agora.


1. O Mistério do Array de Dependências (Por Que Às Vezes Não Funciona)

A Verdade Simples

React compara cada dependência usando Object.is(). Isso significa:

  • Primitivos (string, number, boolean): Compara pelo valor ✅

  • Objetos e arrays: Compara pela referência na memória ❌

Por que isso importa? Objetos e arrays são recriados a cada render, então mesmo com conteúdo idêntico, React acha que mudou.

Vamos Ver Na Prática

/* ================================================
 * ❌ PROBLEMA: Por que isso não funciona?
 * 
 * O objeto filters é RECRIADO a cada render.
 * Mesmo que os valores sejam iguais, a referência
 * na memória é diferente.
 * 
 * Resultado: useMemo SEMPRE recalcula
 * ================================================ */

function ListaProdutos({ filtros, produtos }) {
  const produtosFiltrados = useMemo(() => {
    console.log('🔄 Filtrando produtos...');
    return produtos.filter(produto => 
      filtros.categorias.includes(produto.categoria) &&
      produto.preco >= filtros.precoMinimo
    );
  }, [filtros, produtos]); // ❌ filtros muda toda hora
  
  return <Lista items={produtosFiltrados} />;
}

// Cada render cria um NOVO objeto filtros
<ListaProdutos 
  filtros={{ categorias: ['eletrônicos'], precoMinimo: 100 }} 
  produtos={produtos} 
/>

O que acontece: Console mostra "🔄 Filtrando produtos..." em TODA render, mesmo quando filtros não mudaram de verdade.

A Solução Que Funciona

/* ================================================
 * 💡 EXPLICAÇÃO SIMPLES:
 * 
 * Ao invés de passar objeto inteiro, extraímos
 * os valores primitivos (string, number).
 * 
 * React consegue comparar esses valores corretamente
 * e só recalcula quando REALMENTE mudarem.
 * ================================================ */

function ListaProdutos({ filtros, produtos }) {
  // Extrair valores primitivos
  const { categorias, precoMinimo } = filtros;
  const categoriasString = categorias.join(','); // String é primitivo
  
  const produtosFiltrados = useMemo(() => {
    console.log('🔄 Filtrando produtos...');
    return produtos.filter(produto => 
      categorias.includes(produto.categoria) &&
      produto.preco >= precoMinimo
    );
  }, [categoriasString, precoMinimo, produtos]); // ✅ Só primitivos
  
  return <Lista items={produtosFiltrados} />;
}

Agora sim: Console só mostra "🔄 Filtrando produtos..." quando categorias ou preço REALMENTE mudarem.

Quando Isso É Útil

  • Filtros de busca: Evita reprocessar lista inteira a cada digitação

  • Dashboards: Cálculos caros só rodam quando dados mudam

  • Formulários complexos: Validações pesadas não travam o input

⚠️ Cuidado Com

  • Arrays grandes: join() em 10.000 items pode ser caro

  • Objetos aninhados: Extrair todos os valores fica complexo

  • Performance trade-off: Às vezes é melhor recalcular que fazer malabarismo


2. O Erro De Memoizar Tudo (Quando NÃO Usar useMemo)

A Verdade Simples

useMemo tem custo: ele precisa comparar dependências e guardar valor na memória. Se a operação for simples, o custo do useMemo é MAIOR que simplesmente recalcular.

Regra prática: Só memoize se a operação demorar mais de 1ms.

Vamos Ver O Problema

/* ================================================
 * ❌ PROBLEMA: Over-optimization
 * 
 * Essas operações são MUITO rápidas (< 0.01ms).
 * useMemo está consumindo memória e fazendo comparações
 * desnecessárias.
 * 
 * Resultado: App usa mais memória sem ganho real
 * ================================================ */

function PerfilUsuario({ usuario, preferencias }) {
  const nomeUsuario = useMemo(() => usuario.nome, [usuario.nome]); // ❌
  const idade = useMemo(() => 2024 - usuario.anoNascimento, [usuario.anoNascimento]); // ❌
  const isAdmin = useMemo(() => usuario.role === 'admin', [usuario.role]); // ❌
  
  // ✅ Esse sim vale a pena memoizar (operação cara)
  const estatisticasComplexas = useMemo(() => {
    return calcularAnaliseCompleta(usuario.atividade, preferencias.metricas);
  }, [usuario.atividade, preferencias.metricas]);
  
  return (
    <div>
      {nomeUsuario} - {idade} anos - {isAdmin ? 'Admin' : 'User'}
      <GraficoEstatisticas dados={estatisticasComplexas} />
    </div>
  );
}

O que acontece: App usa memória extra para guardar valores que são instantâneos de calcular.

O Jeito Certo

/* ================================================
 * 💡 EXPLICAÇÃO SIMPLES:
 * 
 * Operações baratas: sem useMemo
 * Operações caras: com useMemo
 * 
 * Como saber? Se demora menos de 1ms, não memoize.
 * Use console.time() para medir.
 * ================================================ */

function PerfilUsuario({ usuario, preferencias }) {
  // ✅ Operações simples: sem useMemo
  const nomeUsuario = usuario.nome;
  const idade = 2024 - usuario.anoNascimento;
  const isAdmin = usuario.role === 'admin';
  
  // ✅ Operação cara: COM useMemo
  const estatisticasComplexas = useMemo(() => {
    console.time('Cálculo Estatísticas');
    const resultado = calcularAnaliseCompleta(usuario.atividade, preferencias.metricas);
    console.timeEnd('Cálculo Estatísticas'); // Mostra tempo no console
    return resultado;
  }, [usuario.atividade, preferencias.metricas]);
  
  return (
    <div>
      {nomeUsuario} - {idade} anos - {isAdmin ? 'Admin' : 'User'}
      <GraficoEstatisticas dados={estatisticasComplexas} />
    </div>
  );
}

Resultado: App usa menos memória e performance melhora porque não há overhead de comparações desnecessárias.

Como Decidir

✅ MEMOIZE quando:

  • Processar array com 1000+ items

  • Cálculos matemáticos complexos

  • Transformações de dados pesadas

  • Operações que demoram > 1ms

❌ NÃO MEMOIZE quando:

  • Acessar propriedade simples (user.name)

  • Operações matemáticas básicas (2024 - year)

  • Comparações simples (role === 'admin')

  • Concatenar strings curtas

Ferramenta Útil

// Hook pra medir se vale a pena memoizar
function useMemoWithProfiling(factory, deps, nome) {
  return useMemo(() => {
    const inicio = performance.now();
    const resultado = factory();
    const fim = performance.now();
    
    console.log(`${nome}: ${(fim - inicio).toFixed(2)}ms`);
    
    return resultado;
  }, deps);
}

// Uso
const dados = useMemoWithProfiling(
  () => processarDadosGrandes(rawData),
  [rawData],
  'Processar Dados'
);

3. Funções Que Sempre Mudam (O Problema Das Callbacks)

A Verdade Simples

Funções são objetos em JavaScript. Cada render cria uma NOVA função, mesmo que o código seja idêntico.

Isso causa re-renders em componentes filhos que usam React.memo.

Vamos Ver O Problema

/* ================================================
 * ❌ PROBLEMA: Novas funções em cada render
 * 
 * Cada render cria NOVAS funções onToggle e onDelete.
 * Componentes TodoItem re-renderizam mesmo que
 * o todo não tenha mudado.
 * 
 * Resultado: Lista com 100 items = 100 re-renders
 * ================================================ */

function ListaTodos({ todos, onUpdate }) {
  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={() => onUpdate(todo.id, !todo.completo)} // ❌ Nova função
          onDelete={() => onUpdate(todo.id, null)} // ❌ Nova função
        />
      ))}
    </div>
  );
}

// TodoItem está usando React.memo mas não adianta
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  console.log(`Renderizando ${todo.id}`);
  return (
    <div>
      <span>{todo.texto}</span>
      <button onClick={onToggle}>✓</button>
      <button onClick={onDelete}>🗑️</button>
    </div>
  );
});

O que acontece: Console mostra todos os items sendo renderizados, mesmo que só um tenha mudado.

A Solução

/* ================================================
 * 💡 EXPLICAÇÃO SIMPLES:
 * 
 * Ao invés de criar funções novas, criamos uma
 * "fábrica de funções" memoizada.
 * 
 * A fábrica retorna sempre a MESMA função para
 * cada todo.id, evitando re-renders desnecessários.
 * ================================================ */

function ListaTodos({ todos, onUpdate }) {
  // Fábrica de funções memoizada
  const criarHandlerToggle = useMemo(() => 
    (todoId, completo) => () => onUpdate(todoId, completo)
  , [onUpdate]);
  
  const criarHandlerDelete = useMemo(() => 
    (todoId) => () => onUpdate(todoId, null)
  , [onUpdate]);
  
  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={criarHandlerToggle(todo.id, !todo.completo)}
          onDelete={criarHandlerDelete(todo.id)}
        />
      ))}
    </div>
  );
}

const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  console.log(`Renderizando ${todo.id}`);
  return (
    <div>
      <span>{todo.texto}</span>
      <button onClick={onToggle}>✓</button>
      <button onClick={onDelete}>🗑️</button>
    </div>
  );
});

Agora sim: Console só mostra o item que realmente mudou sendo renderizado.

Quando Isso É Útil

  • Listas grandes: Grids com 100+ items

  • Componentes React.memo: Que precisam estabilidade de props

  • Drag & Drop: Handlers precisam ser estáveis

  • Virtualization: react-window, react-virtualized

⚠️ Alternativa Mais Simples

Para a maioria dos casos, useCallback é mais direto:

const handleClick = useCallback((id) => {
  onUpdate(id);
}, [onUpdate]);

Use useMemo quando precisar retornar algo além de função.


4. Memoização Inteligente (Só Ativar Quando Necessário)

A Verdade Simples

Não faz sentido memoizar valores de componentes que nem estão visíveis na tela. Isso desperdiça memória.

Vamos Ver O Problema

/* ================================================
 * ❌ PROBLEMA: Memoizando mesmo invisível
 * 
 * Component está escondido (display: none) mas
 * continua processando dados pesados.
 * 
 * Resultado: Memória e CPU desperdiçados
 * ================================================ */

function PainelDados({ dados, isVisivel }) {
  const dadosProcessados = useMemo(() => {
    console.log('🔄 Processando dados...');
    return processamentoMuitoPesado(dados);
  }, [dados]); // Roda mesmo quando isVisivel = false
  
  if (!isVisivel) {
    return null; // Componente não renderiza mas já processou tudo
  }
  
  return <Grafico dados={dadosProcessados} />;
}

O que acontece: Console mostra "🔄 Processando dados..." mesmo quando painel está escondido.

A Solução

/* ================================================
 * 💡 EXPLICAÇÃO SIMPLES:
 * 
 * Só processar quando realmente necessário:
 * - Componente está visível
 * - Dados são grandes (> 100 items)
 * 
 * Se não precisa agora, não processa.
 * ================================================ */

function PainelDados({ dados, isVisivel }) {
  const dadosGrandes = dados.length > 100;
  const deveMemoizar = isVisivel && dadosGrandes;
  
  const dadosProcessados = useMemo(() => {
    if (!deveMemoizar) {
      return processamentoMuitoPesado(dados);
    }
    console.log('🔄 Processando dados (memoizado)...');
    return processamentoMuitoPesado(dados);
  }, deveMemoizar ? [dados] : []); // Array vazio = sempre recalcula
  
  if (!isVisivel) {
    return null;
  }
  
  return <Grafico dados={dadosProcessados} />;
}

Resultado: Processamento só acontece quando painel fica visível.

Exemplo Mais Prático

// Hook customizado pra memoização inteligente
function useMemoizacaoInteligente(factory, deps, condicoes) {
  const { isVisivel = true, tamanhoMinimo = 0 } = condicoes;
  
  const deveMemoizar = isVisivel && 
    (deps[0]?.length || 0) >= tamanhoMinimo;
  
  return useMemo(() => {
    if (!deveMemoizar) {
      return factory();
    }
    return factory();
  }, deveMemoizar ? deps : []);
}

// Uso
function PainelDados({ dados, isVisivel }) {
  const dadosProcessados = useMemoizacaoInteligente(
    () => processamentoMuitoPesado(dados),
    [dados],
    { isVisivel, tamanhoMinimo: 100 }
  );
  
  if (!isVisivel) return null;
  return <Grafico dados={dadosProcessados} />;
}

Quando Isso É Útil

  • Tabs/Modais: Só processar tab ativa

  • Scroll infinito: Só processar items visíveis

  • Dashboards: Só calcular widgets expandidos

  • Mobile: Economizar bateria/CPU


5. Cálculos Em Cadeia (Como Não Reprocessar Tudo)

A Verdade Simples

Se você tem vários cálculos que dependem uns dos outros, pode separar em múltiplos useMemo. Assim, se só uma parte mudar, as outras não recalculam.

Vamos Ver O Problema

/* ================================================
 * ❌ PROBLEMA: Um useMemo fazendo tudo
 * 
 * Se chartConfig mudar (coisa barata), TUDO
 * é recalculado: filtro caro, agregação cara,
 * e formatação barata.
 * 
 * Resultado: Mudou cor do gráfico = recalcula tudo
 * ================================================ */

function Dashboard({ dadosRaw, filtros, chartConfig }) {
  const dadosProntos = useMemo(() => {
    console.log('🔄 TUDO sendo recalculado...');
    const filtrados = aplicarFiltros(dadosRaw, filtros); // Caro
    const agregados = agregarMetricas(filtrados); // Caro
    const formatados = formatarParaGrafico(agregados, chartConfig); // Barato
    return formatados;
  }, [dadosRaw, filtros, chartConfig]); // ❌ chartConfig força tudo
  
  return <Grafico dados={dadosProntos} />;
}

O que acontece: Mudar cor do gráfico reprocessa milhares de registros.

A Solução

/* ================================================
 * 💡 EXPLICAÇÃO SIMPLES:
 * 
 * Separar em camadas:
 * 1. Operação mais cara (filtro)
 * 2. Operação cara (agregação)
 * 3. Operação barata (formatação)
 * 
 * Cada uma só recalcula se SUA dependência mudar.
 * ================================================ */

function Dashboard({ dadosRaw, filtros, chartConfig }) {
  // Camada 1: Mais cara, depende de dados + filtros
  const dadosFiltrados = useMemo(() => {
    console.log('🔄 Filtrando dados...');
    return aplicarFiltros(dadosRaw, filtros);
  }, [dadosRaw, filtros]);
  
  // Camada 2: Cara, depende só dos dados filtrados
  const dadosAgregados = useMemo(() => {
    console.log('🔄 Agregando métricas...');
    return agregarMetricas(dadosFiltrados);
  }, [dadosFiltrados]);
  
  // Camada 3: Barata, depende de agregados + config
  const dadosFormatados = useMemo(() => {
    console.log('🔄 Formatando para gráfico...');
    return formatarParaGrafico(dadosAgregados, chartConfig);
  }, [dadosAgregados, chartConfig]); // ✅ Só reroda formatação
  
  return <Grafico dados={dadosFormatados} />;
}

Resultado: Mudar cor do gráfico só roda formatação (barata). Filtro e agregação não rodam de novo.

Visualização Do Que Acontece

chartConfig muda:
❌ Antes: Filtro → Agregação → Formatação (TUDO)
✅ Depois: Formatação (SÓ ISSO)

filtros muda:
✅ Filtro → Agregação → Formatação (necessário)

dadosRaw muda:
✅ Filtro → Agregação → Formatação (necessário)

Quando Isso É Útil

  • Pipelines de dados: ETL, transformações múltiplas

  • Dashboards complexos: Filtros + cálculos + formatação

  • Visualizações: Dados → Escalas → Renderização

  • Formulários: Validação → Formatação → Submit


6. Operações Assíncronas (Evitar Requisições Duplicadas)

A Verdade Simples

useMemo é síncrono, mas você pode usá-lo pra evitar iniciar a MESMA operação assíncrona múltiplas vezes.

Vamos Ver O Problema

/* ================================================
 * ❌ PROBLEMA: Múltiplas requisições iguais
 * 
 * Cada render pode disparar nova requisição,
 * mesmo que userId seja o mesmo.
 * 
 * Resultado: API recebe 10 chamadas idênticas
 * ================================================ */

function PerfilUsuario({ userId }) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    setLoading(true);
    buscarDadosUsuario(userId).then(dados => {
      setUserData(dados);
      setLoading(false);
    });
  }, [userId]); // Re-roda toda vez
  
  if (loading) return <div>Carregando...</div>;
  return <div>{userData?.nome}</div>;
}

O que acontece: Durante desenvolvimento (StrictMode), React renderiza 2x, disparando 2 requisições.

A Solução

/* ================================================
 * 💡 EXPLICAÇÃO SIMPLES:
 * 
 * Criar hook que:
 * 1. Guarda resultado da requisição
 * 2. Só faz nova requisição se userId mudar
 * 3. Cancela requisição se componente desmontar
 * ================================================ */

function useAsyncMemo(asyncFactory, deps) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });
  
  // Memoiza a última combinação de deps vista
  const depsString = deps.map(d => JSON.stringify(d)).join(',');
  
  useEffect(() => {
    let cancelado = false;
    
    asyncFactory()
      .then(data => {
        if (!cancelado) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelado) {
          setState({ data: null, loading: false, error });
        }
      });
    
    return () => {
      cancelado = true; // Cancela se componente desmontar
    };
  }, [depsString]);
  
  return state;
}

// Uso
function PerfilUsuario({ userId }) {
  const { data: userData, loading, error } = useAsyncMemo(
    () => buscarDadosUsuario(userId),
    [userId]
  );
  
  if (loading) return <div>Carregando...</div>;
  if (error) return <div>Erro: {error.message}</div>;
  return <div>{userData?.nome}</div>;
}

Resultado: Só uma requisição por userId, mesmo com re-renders.

Versão Com Cache

// Hook com cache de 5 minutos
function useAsyncMemoComCache(asyncFactory, deps, tempoCacheMs = 5 * 60 * 1000) {
  const cache = useRef(new Map());
  const [, forceUpdate] = useState(0);
  
  const cacheKey = JSON.stringify(deps);
  const cached = cache.current.get(cacheKey);
  
  const estaNovo = cached && 
    (Date.now() - cached.timestamp < tempoCacheMs);
  
  useEffect(() => {
    if (estaNovo) return; // Usa cache
    
    let cancelado = false;
    
    cache.current.set(cacheKey, {
      data: cached?.data || null,
      loading: true,
      timestamp: Date.now()
    });
    forceUpdate(n => n + 1);
    
    asyncFactory()
      .then(data => {
        if (!cancelado) {
          cache.current.set(cacheKey, {
            data,
            loading: false,
            timestamp: Date.now()
          });
          forceUpdate(n => n + 1);
        }
      })
      .catch(error => {
        if (!cancelado) {
          cache.current.set(cacheKey, {
            data: null,
            loading: false,
            error,
            timestamp: Date.now()
          });
          forceUpdate(n => n + 1);
        }
      });
    
    return () => {
      cancelado = true;
    };
  }, [cacheKey, estaNovo]);
  
  return cache.current.get(cacheKey) || { 
    data: null, 
    loading: true 
  };
}

// Uso
function PerfilUsuario({ userId }) {
  const { data, loading, error } = useAsyncMemoComCache(
    () => buscarDadosUsuario(userId),
    [userId],
    5 * 60 * 1000 // 5 minutos de cache
  );
  
  if (loading) return <div>Carregando...</div>;
  if (error) return <div>Erro: {error.message}</div>;
  return <div>{data?.nome}</div>;
}

Quando Isso É Útil

  • Dados de usuário: Perfil, preferências

  • Listas estáticas: Categorias, países

  • Configurações: App settings, feature flags

  • Buscas: Autocomplete, sugestões

⚠️ Para Produção

Use bibliotecas como:

  • React Query: Cache, refetch, mutations

  • SWR: Stale-while-revalidate pattern

  • Redux Toolkit Query: Integrado com Redux


Você Consegue! 🎉

Agora você sabe a verdade sobre useMemo:

  1. Array de dependências precisa de valores que React consiga comparar

  2. Nem tudo deve ser memoizado - só operações caras

  3. Funções são objetos - precisam de estratégias especiais

  4. Memoização inteligente economiza recursos

  5. Cálculos em cadeia evitam reprocessamento total

  6. Operações assíncronas precisam de cuidados extras

Não é sobre decorar sintaxe. É sobre entender quando e por que usar cada técnica.

Próximo Passo

Abra um projeto seu e:

  1. Adicione console.time() em cálculos que você acha caros

  2. Meça se realmente demoram > 1ms

  3. Aplique useMemo só nos que valerem a pena

  4. Use React DevTools Profiler pra ver diferença real

Qualquer dúvida, a comunidade React está aí pra ajudar. Todo mundo já se confundiu com isso! 💪


Materiais de Referência

  1. React Profiler DevTools - Ferramenta oficial para medir performance: https://react.dev/reference/react/Profiler

  2. Web Performance APIs - Especificação W3C para medição de performance: https://www.w3.org/webperf/

  3. React Concurrent Features RFC - Documentação técnica oficial sobre features concorrentes: https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md