Entendendo Memoização em React
Memoização é uma técnica de otimização que consiste em armazenar o resultado de uma operação custosa e reutilizá-lo quando os mesmos parâmetros são fornecidos novamente. Em React, essa prática é fundamental quando você trabalha com componentes complexos ou aplicações que sofrem com renderizações desnecessárias.
Antes de mergulhar em useMemo e useCallback, é crucial compreender que React renderiza componentes sempre que seu estado ou props mudam. Em muitos casos, isso é eficiente e desejável. Porém, quando você tem cálculos pesados, grandes listas de dados ou funções que são dependências críticas de outros efeitos, memoização torna-se uma ferramenta poderosa. O ponto central é este: memoização não resolve todos os problemas de performance e, quando usada incorretamente, pode até piorá-los.
useMemo: Guardando Resultados de Cálculos
Conceito e Sintaxe
useMemo é um Hook do React que memoriza um valor calculado e só o recalcula quando suas dependências mudam. A sintaxe é simples: você passa uma função que retorna um valor e um array de dependências. Se as dependências não mudarem, React retorna o valor anterior armazenado em memória, economizando processamento.
const memoizedValue = useMemo(() => {
return expensiveCalculation(a, b);
}, [a, b]);
Um Exemplo Prático Real
Imagine um aplicativo que filtra uma lista de usuários e calcula estatísticas. Sem memoização, a filtragem aconteceria em cada renderização, mesmo que os dados não tivessem mudado:
import React, { useState, useMemo } from 'react';
function UserAnalytics({ users, searchTerm }) {
// ❌ SEM useMemo: filterUsers é recalculado a cada renderização
// const filteredUsers = users.filter(user =>
// user.name.toLowerCase().includes(searchTerm.toLowerCase())
// );
// ✅ COM useMemo: filterUsers só é recalculado se users ou searchTerm mudam
const filteredUsers = useMemo(() => {
console.log('Filtrando usuários...');
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
// Cálculo custoso de estatísticas
const statistics = useMemo(() => {
console.log('Calculando estatísticas...');
return {
total: filteredUsers.length,
avgAge: filteredUsers.reduce((sum, u) => sum + u.age, 0) / filteredUsers.length || 0,
oldest: Math.max(...filteredUsers.map(u => u.age), 0),
};
}, [filteredUsers]);
return (
<div>
<h2>Total: {statistics.total}</h2>
<h2>Idade Média: {statistics.avgAge.toFixed(2)}</h2>
<h2>Mais Velho: {statistics.oldest}</h2>
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name} - {user.age} anos</li>
))}
</ul>
</div>
);
}
export default UserAnalytics;
Neste exemplo, sem useMemo, se o componente pai re-renderizar por qualquer motivo (como uma mudança de tema ou outro estado), os filtros e cálculos aconteceriam novamente desnecessariamente. Com useMemo, apenas quando users ou searchTerm realmente mudarem é que o cálculo é executado.
O Custo Real da Memoização
Aqui está o ponto crítico que muitos desenvolvedores ignoram: memoização tem um custo. React precisa comparar as dependências a cada renderização, e se o valor memorizado for primitivo ou muito simples, o overhead dessa comparação pode ser maior que o benefício. Além disso, armazenar em memória consome recursos.
Use useMemo quando você tiver certeza de que:
- O cálculo é realmente custoso (operações com arrays grandes, cálculos matemáticos complexos)
- As dependências mudam com frequência menor que a renderização do componente
- O valor é passado como props a componentes que usam React.memo
useCallback: Memoizando Funções
Por Que Memoizar Funções?
Funções em JavaScript são objetos. Cada vez que seu componente renderiza, uma nova função é criada, mesmo que o corpo da função seja idêntico. Isso parece inofensivo, mas há cenários onde causa problemas reais: quando você passa uma função como prop para um componente React.memo, a nova função quebra a memoização daquele componente, causando renderizações desnecessárias.
useCallback permite que você retorne a mesma instância de função entre renderizações, desde que as dependências não mudem:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Exemplo Prático com Componentes Memoizados
import React, { useState, useCallback, memo } from 'react';
// Componente que renderiza lista de botões
// Usamos memo porque queremos evitar renderizações desnecessárias
const ButtonList = memo(({ items, onItemClick }) => {
console.log('ButtonList renderizado');
return (
<div>
{items.map(item => (
<button
key={item.id}
onClick={() => onItemClick(item.id)}
>
{item.label}
</button>
))}
</div>
);
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [selectedId, setSelectedId] = useState(null);
// ❌ SEM useCallback: ButtonList renderiza a cada clique em count
// const handleItemClick = (id) => {
// setSelectedId(id);
// console.log('Item clicado:', id);
// };
// ✅ COM useCallback: ButtonList só renderiza se items mudar
const handleItemClick = useCallback((id) => {
setSelectedId(id);
console.log('Item clicado:', id);
}, []);
const items = [
{ id: 1, label: 'Item A' },
{ id: 2, label: 'Item B' },
{ id: 3, label: 'Item C' },
];
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Incrementar
</button>
<p>Selected ID: {selectedId}</p>
<ButtonList items={items} onItemClick={handleItemClick} />
</div>
);
}
export default ParentComponent;
Abra o console e clique no botão "Incrementar". Sem useCallback, você veria "ButtonList renderizado" a cada clique, porque uma nova função é passada como prop. Com useCallback, ButtonList só renderiza se a função realmente mudar (ou se items mudar, neste caso não muda).
Dependências e Armadilhas Comuns
O maior erro ao usar useCallback é esquecer de incluir dependências que a função realmente usa. Se sua callback precisa acessar uma variável externa, essa variável deve estar no array de dependências:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
// ❌ BUG: step não está nas dependências
// const incrementByStep = useCallback(() => {
// setCount(count + step); // count e step são acessados mas não estão nas deps
// }, [count]);
// ✅ CORRETO: todas as dependências incluídas
const incrementByStep = useCallback(() => {
setCount(prevCount => prevCount + step);
}, [step]);
return (
<div>
<p>Count: {count}</p>
<p>Step: {step}</p>
<button onClick={incrementByStep}>Incrementar por {step}</button>
<button onClick={() => setStep(step + 1)}>Aumentar Step</button>
</div>
);
}
Note aqui que usamos setCount(prevCount => ...) ao invés de acessar diretamente count. Esta é uma prática importante: sempre que possível, use funções de atualização de estado para evitar dependências desnecessárias.
Análise de Custo: Quando Usar e Quando Evitar
Medir Antes de Otimizar
A regra de ouro da otimização é: meça primeiro. Use as ferramentas de profiling do React (React DevTools Profiler) para identificar onde o tempo está sendo gasto. Muitas vezes, o gargalo não está onde você acha que está.
// Exemplo de como usar React DevTools Profiler
// 1. Abra React DevTools > Profiler
// 2. Grave uma interação
// 3. Procure por componentes que levam mais tempo
// 4. Identifique se é renderização ou execução do componente
function ExpensiveComponent({ data }) {
// Um cálculo que realmente é custoso
const result = useMemo(() => {
let sum = 0;
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < 1000000; j++) {
sum += data[i] * j;
}
}
return sum;
}, [data]);
return <div>{result}</div>;
}
Tabela de Custos
| Situação | useMemo | useCallback | Recomendação |
|---|---|---|---|
| Cálculo simples (strings, números pequenos) | ❌ | ❌ | Não use memoização |
| Filtro/mapa de grande array | ✅ | - | Use useMemo |
| Função passada a React.memo | - | ✅ | Use useCallback |
| Dependência de useEffect custoso | ✅ | ✅ | Use conforme necessário |
| Objeto/array como prop | ✅ | - | Use useMemo para o objeto/array |
| Estado derivado simples | ❌ | ❌ | Apenas calcule inline |
Exemplo: Decisão de Memoização
function Dashboard({ userId }) {
const [filters, setFilters] = useState({});
// ❌ EVITE: Comparação é mais cara que o cálculo
const userInitials = useMemo(() => {
return userId.split(' ').map(n => n[0]).join('');
}, [userId]);
// ✅ BOAS: Array grande que será processado
const processedData = useMemo(() => {
return largeDataset
.filter(item => item.userId === userId)
.map(item => ({
...item,
score: calculateComplexScore(item),
}));
}, [userId, largeDataset]);
// ✅ BOA: Função passada a componente memoizado
const handleFilter = useCallback((newFilter) => {
setFilters(prev => ({ ...prev, ...newFilter }));
}, []);
return (
<div>
<h1>{userInitials}</h1>
<DataTable data={processedData} onFilter={handleFilter} />
</div>
);
}
Conclusão
Três aprendizados principais levam você a dominar memoização em React:
-
Memoização é uma ferramenta, não uma solução universal. Use-a estrategicamente apenas quando mensurar e confirmar que há ganho real. Adicionar
useMemoeuseCallbackem tudo é anti-pattern e prejudica performance. -
As dependências são críticas e exigem atenção. Esquecer ou incluir dependências erradas quebra a lógica do componente e causa bugs sutis. Use ferramentas como ESLint plugin para React Hooks para evitar erros.
-
Entenda o custo: comparação de dependências, armazenamento em memória e complexidade do código. Às vezes, recalcular é mais barato que memoizar. Perfil suas aplicações com React DevTools antes de aplicar otimizações.