useMemo e useCallback: Memoização Real com Análise de Custo na Prática Já leu

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 e , é 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 é 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

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:

  1. Memoização é uma ferramenta, não uma solução universal. Use-a estrategicamente apenas quando mensurar e confirmar que há ganho real. Adicionar useMemo e useCallback em tudo é anti-pattern e prejudica performance.

  2. 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.

  3. 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.

Referências


Artigos relacionados