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:
Array de dependências funciona diferente do que você espera
Nem tudo deve ser memoizado (às vezes piora performance)
Comparação de objetos não funciona como primitivos
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 caroObjetos 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:
Array de dependências precisa de valores que React consiga comparar
Nem tudo deve ser memoizado - só operações caras
Funções são objetos - precisam de estratégias especiais
Memoização inteligente economiza recursos
Cálculos em cadeia evitam reprocessamento total
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:
Adicione
console.time()em cálculos que você acha carosMeça se realmente demoram > 1ms
Aplique
useMemosó nos que valerem a penaUse 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
React Profiler DevTools - Ferramenta oficial para medir performance: https://react.dev/reference/react/Profiler
Web Performance APIs - Especificação W3C para medição de performance: https://www.w3.org/webperf/
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
