portfólio
← voltar

useMemo e useCallback: memoização e identidade referencial no React

Entenda por que objetos, arrays e funções mudam a cada render, como React.memo, useMemo e useCallback funcionam por baixo dos panos, e quando realmente vale usá-los.

Matheus Pergoli··11 min de leitura

O React re-renderiza componentes com frequência. E isso não é um problema — é o mecanismo que mantém a interface sincronizada com o estado da aplicação.

O problema aparece quando re-renders acontecem sem necessidade: trabalho pesado sendo refeito desnecessariamente, componentes redesenhados sem que nada tenha mudado de verdade. É aí que useMemo e useCallback entram.

Só que esses dois hooks têm uma má reputação de confusão. Desenvolvedores os aplicam em todo lugar "por precaução", ou evitam completamente porque "não entendem quando usar". Nos dois casos, o modelo mental está incompleto.

Este artigo parte do zero. A ideia é construir o raciocínio inteiro — desde o que é um render até quando faz sentido memoizar — sem pular etapas.

O que um re-render realmente é

Para entender o problema que useMemo e useCallback resolvem, é preciso entender o que acontece quando o React re-renderiza um componente.

Um componente React, no fundo, é uma função JavaScript:

JSX
function Contador() {
  const [count, setCount] = React.useState(0)
 
  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

Toda vez que o estado muda, o React chama essa função de novo. Isso é um re-render.

E aqui está o ponto central: cada chamada dessa função cria um ambiente de execução novo.

Isso significa que toda variável declarada dentro do componente é recriada do zero a cada render:

JSX
function Contador() {
  const [count, setCount] = React.useState(0)
 
  // Esse objeto é criado do zero em cada render
  const estilo = { color: "red", fontWeight: "bold" }
 
  // Essa função é criada do zero em cada render
  function handleClick() {
    setCount(count + 1)
  }
 
  return <button style={estilo} onClick={handleClick}>{count}</button>
}

Não é um comportamento específico do React. É como o JavaScript funciona: cada invocação de função cria seu próprio escopo, com suas próprias variáveis.

Por que isso importa: identidade referencial

Em JavaScript, nem todos os valores são comparados da mesma forma.

Primitivos — string, number, boolean — são comparados pelo valor em si:

JS
42 === 42           // true
"texto" === "texto" // true
true === true       // true

Objetos, arrays e funções são comparados pela referência — ou seja, pelo endereço na memória, não pelo conteúdo:

JS
{ color: "red" } === { color: "red" } // false
[1, 2, 3] === [1, 2, 3]               // false
(() => {}) === (() => {})              // false

Essas expressões produzem valores com estrutura idêntica. Mas cada uma cria uma nova entidade na memória. E o operador ===, para objetos, arrays e funções, não compara o conteúdo — compara se os dois lados apontam para o mesmo lugar.

Aplicando isso ao componente anterior:

JSX
function Contador() {
  // Render 1: estilo aponta para o endereço 0x01 na memória
  // Render 2: estilo aponta para o endereço 0x02 na memória
  // Render 3: estilo aponta para o endereço 0x03 na memória
  const estilo = { color: "red", fontWeight: "bold" }

O conteúdo de estilo não mudou em nenhum desses renders. Mas a referência mudou em todos. Para qualquer comparação que use ===, esses valores são sempre diferentes.

E o React usa exatamente esse tipo de comparação em lugares críticos.

Como o React decide o que re-renderizar

Por padrão, quando um componente pai re-renderiza, todos os filhos também re-renderizam. O React não verifica se as props mudaram — simplesmente executa os filhos de novo.

Isso é intencional: re-renders são baratos na maioria dos casos, e a verificação automática evitaria trabalho desnecessário só em situações específicas.

Mas existe uma saída quando um componente filho não precisa ser re-renderizado junto com o pai: React.memo.

JSX
const Filho = React.memo(({ titulo }) => {
  return <h1>{titulo}</h1>
})

Quando um componente é envolvido com React.memo, o React passa a comparar as props antes de decidir se re-renderiza.

Se as props não mudaram, o React reutiliza o resultado do último render e pula a execução.

Como React.memo compara props por dentro

A comparação que o React.memo faz não é profunda. Ela é superficialshallow equality.

O algoritmo percorre as chaves do objeto de props e compara cada valor com ===. A versão simplificada abaixo deixa o mecanismo claro — o React usa Object.is internamente, que se comporta de forma idêntica ao === para a grande maioria dos valores:

JS
// Simplificação didática do que React.memo faz internamente
function shallowEqual(propsAnterior, propsNova) {
  const chavesAnterior = Object.keys(propsAnterior)
  const chavesNova = Object.keys(propsNova)
 
  if (chavesAnterior.length !== chavesNova.length) return false
 
  for (const chave of chavesAnterior) {
    if (propsAnterior[chave] !== propsNova[chave]) return false
  }
 
  return true
}

Se shallowEqual retornar true, o componente não re-renderiza.

Isso tem uma consequência direta: para primitivos, funciona perfeitamente. Para objetos, arrays e funções, quase sempre falha — porque, como vimos, cada render cria novas referências.

JSX
function Pai() {
  const [count, setCount] = React.useState(0)
 
  // Nova referência a cada render
  const config = { tema: "dark" }
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>+</button>
      {/* React.memo não vai proteger esse componente */}
      <FilhoMemo config={config} />
    </>
  )
}

Toda vez que Pai re-renderiza — mesmo que config tenha o mesmo conteúdo —, FilhoMemo recebe uma referência nova. O shallowEqual compara propsAnterior.config !== propsNova.config e retorna false. O componente re-renderiza.

O React.memo está aplicado, mas nunca "ativa" de verdade.

useMemo — o que ele faz

Aqui entra o useMemo.

A ideia central é simples: em vez de recriar um valor a cada render, o React guarda o resultado e só recalcula quando algo relevante muda.

JSX
const valor = React.useMemo(() => {
  return calcularAlgumaCoisaPesada(entrada)
}, [entrada])

useMemo recebe dois argumentos:

  1. Uma função que retorna o valor a ser memoizado
  2. Um array de dependências

Na primeira execução, o React chama a função e guarda o resultado.

Nos renders seguintes, o React compara as dependências com as do render anterior usando ===. Se nenhuma mudou, ele descarta a função e retorna o valor que já tinha guardado. Se alguma mudou, chama a função de novo.

É um cache. As dependências são a estratégia de invalidação desse cache.

Caso de uso 1: cálculos pesados

O exemplo mais direto é uma computação que custa tempo e não precisa ser refeita em todo render.

JSX
function App() {
  const [numero, setNumero] = React.useState(100)
  const [nome, setNome] = React.useState("")
 
  // Sem useMemo: recalcula em todo render, incluindo quando `nome` muda
  const primos = calcularPrimos(numero)
 
  return (
    <>
      <input value={nome} onChange={e => setNome(e.target.value)} />
      <input
        type="number"
        value={numero}
        onChange={e => setNumero(Number(e.target.value))}
      />
      <p>Primos até {numero}: {primos.join(", ")}</p>
    </>
  )
}

Se o usuário digita no campo nome, o componente re-renderiza e calcularPrimos roda de novo — mesmo que numero não tenha mudado.

Com useMemo:

JSX
const primos = React.useMemo(() => {
  return calcularPrimos(numero)
}, [numero]) // só recalcula quando `numero` muda

Agora a computação pesada só acontece quando numero muda. Digitar no campo nome não a dispara mais.

Quando esse uso realmente vale

Aqui vale ser honesto: a maioria das computações em componentes React é rápida demais para o useMemo fazer diferença mensurável.

Filtrar um array de 50 itens, formatar uma data, somar valores — essas operações levam microssegundos. O próprio overhead do useMemo (guardar resultado, comparar dependências a cada render) pode ser maior do que o custo da operação que você está evitando.

Uma régua prática: se a operação consistentemente demora mais de 1ms, começa a valer considerar. Se não, useMemo provavelmente está adicionando complexidade sem retorno real.

Você pode medir isso diretamente:

JS
const resultado = React.useMemo(() => {
  console.time("calculo")
  const r = operacaoPesada(entrada)
  console.timeEnd("calculo")
  return r
}, [entrada])

Se o tempo no console for 0.01ms, o useMemo não está ajudando.

Caso de uso 2: preservar referências

Esse é, na prática, o uso mais importante do useMemo.

O problema é este: quando um componente com React.memo recebe um objeto ou array como prop, o React.memo quase sempre vai falhar — porque cada render cria uma nova referência.

JSX
function App() {
  const [nome, setNome] = React.useState("")
 
  // Novo array a cada render, mesmo quando `nome` muda e `boxes` não deveria
  const boxes = [
    { flex: 1, background: "hsl(345deg 100% 50%)" },
    { flex: 3, background: "hsl(260deg 100% 40%)" },
  ]
 
  return (
    <>
      <input value={nome} onChange={e => setNome(e.target.value)} />
      <BoxesMemo boxes={boxes} />
    </>
  )
}

BoxesMemo está envolto com React.memo. Mas toda vez que o usuário digita no input, App re-renderiza, cria um novo array boxes e passa para BoxesMemo. O shallowEqual vê referências diferentes e re-renderiza o filho.

A solução é manter a mesma referência entre renders:

JSX
const boxes = React.useMemo(() => {
  return [
    { flex: 1, background: "hsl(345deg 100% 50%)" },
    { flex: 3, background: "hsl(260deg 100% 40%)" },
  ]
}, []) // sem dependências: cria o array uma vez e reutiliza sempre

Agora o React guarda esse array no primeiro render e retorna a mesma referência nos renders seguintes. O shallowEqual do React.memo recebe a mesma referência que recebeu antes e não re-renderiza o filho.

A conexão entre useMemo e React.memo

Um detalhe importante: useMemo e React.memo são peças separadas, mas trabalham juntas.

React.memo protege um componente de re-renders desnecessários vindos do pai. useMemo garante que as referências passadas como prop sejam estáveis entre renders.

Sem os dois juntos, um deles não resolve o problema:

useCallback — o mesmo mecanismo para funções

Funções seguem exatamente a mesma lógica de identidade referencial que objetos e arrays.

Cada render cria uma nova função:

JS
// Render 1: handleClick aponta para 0xAA
// Render 2: handleClick aponta para 0xBB
// Render 3: handleClick aponta para 0xCC
function handleClick() {
  setCount(count + 1)
}

O conteúdo é idêntico. A referência é diferente.

Isso significa que se você passa uma função como prop para um componente com React.memo, ele vai re-renderizar a cada vez — pelo mesmo motivo do array de boxes.

JSX
function App() {
  const [count, setCount] = React.useState(0)
 
  // Nova referência a cada render
  function handleBoost() {
    setCount(c => c + 1000)
  }
 
  return (
    <>
      <p>{count}</p>
      {/* MegaBoostMemo re-renderiza toda vez, mesmo com React.memo */}
      <MegaBoostMemo onClick={handleBoost} />
    </>
  )
}

useCallback resolve isso da mesma forma que useMemo resolveu para arrays:

JSX
const handleBoost = React.useCallback(() => {
  setCount(c => c + 1000)
}, []) // sem dependências: mesma função em todos os renders

useCallback é syntactic sugar

Essas duas expressões são funcionalmente equivalentes:

JS
// Com useCallback
const fn = React.useCallback(() => {
  fazerAlgo()
}, [])
 
// Com useMemo retornando uma função
const fn = React.useMemo(() => {
  return () => {
    fazerAlgo()
  }
}, [])

useCallback existe porque a versão com useMemo fica verbosa quando o objetivo é memoizar uma função. É só uma conveniência de leitura — o mecanismo por baixo é idêntico.

A armadilha do stale closure

Quando você memoiza uma função, ela captura as variáveis do escopo em que foi criada. Isso é um closure — comportamento normal do JavaScript.

O problema surge quando você usa dependências vazias e a função lê um valor de estado:

JSX
function Contador() {
  const [count, setCount] = React.useState(0)
 
  // ❌ Stale closure: count está "congelado" no valor do primeiro render
  const handleClick = React.useCallback(() => {
    console.log(count) // sempre 0, não importa quantas vezes o estado mude
    setCount(count + 1) // sempre vai para 1
  }, []) // dependência vazia = função nunca é recriada
 
  return <button onClick={handleClick}>{count}</button>
}

A função foi criada uma vez, com count = 0 no closure. O estado muda, o componente re-renderiza, mas a função memoizada ainda enxerga o count original.

Esse é um stale closure — um closure que enxerga um valor desatualizado da variável que capturou.

Existem duas saídas:

Opção 1: colocar a dependência corretamente.

JSX
const handleClick = React.useCallback(() => {
  setCount(count + 1)
}, [count]) // a função é recriada quando count muda

Isso funciona, mas elimina parte do benefício de memoização — a função vai mudar de referência sempre que count mudar.

Opção 2: functional update.

JSX
const handleClick = React.useCallback(() => {
  setCount(prev => prev + 1) // não lê count do closure
}, []) // dependência vazia, sem stale closure

O functional update recebe o valor atual do estado como argumento dentro do setter. A função não precisa capturar count no closure — ela acessa o valor mais recente pelo parâmetro prev.

Essa é a abordagem correta quando a função só precisa do valor anterior do estado para calcular o próximo.

A regra geral é: se você pode expressar a atualização como prev => próximoValor, use functional update e deixe as dependências vazias. Se você precisa ler o estado por outros motivos, coloque-o nas dependências.

Quando usar esses hooks na prática

Agora que o mecanismo está claro, a pergunta prática: quando vale aplicar?

A resposta honesta é que, na maioria dos casos, não vale.

React é altamente otimizado. Re-renders são, em geral, muito mais rápidos do que desenvolvedores imaginam. Aplicar useMemo e useCallback em todo lugar não é "ser cuidadoso com performance" — é adicionar complexidade, aumentar uso de memória e tornar o código mais difícil de ler sem benefício real.

A abordagem saudável é a inversa: escreva o código simples primeiro. Se você observar lentidão real, meça com o React DevTools Profiler para identificar onde o problema está. Aí sim aplique memoização onde ela claramente ajuda.

Dito isso, há dois cenários onde faz sentido ser preemptivo:

Context providers

Quando um contexto distribui um objeto como value, sem memoização, qualquer re-render do provider vai recriar esse objeto e forçar todos os consumidores do contexto a re-renderizar — mesmo os que estão em React.memo.

JSX
function AuthProvider({ children }) {
  const [user, setUser] = React.useState(null)
  const [status, setStatus] = React.useState("idle")
 
  // ❌ Novo objeto a cada render do provider
  return (
    <AuthContext.Provider value={{ user, status, setUser, setStatus }}>
      {children}
    </AuthContext.Provider>
  )
}

Com useMemo:

JSX
function AuthProvider({ children }) {
  const [user, setUser] = React.useState(null)
  const [status, setStatus] = React.useState("idle")
 
  // ✅ Mesmo objeto enquanto user e status não mudarem
  const value = React.useMemo(() => ({
    user,
    status,
    setUser,   // estável: React garante que setters do useState não mudam
    setStatus, // estável: mesma garantia
  }), [user, status])
 
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}

setUser e setStatus não precisam entrar no array de dependências porque o React garante que as funções retornadas pelo useState são estáveis — a mesma referência em todos os renders.

Custom hooks genéricos

Quando você escreve um hook para ser reutilizado em diferentes partes da aplicação, você não sabe como ele vai ser consumido no futuro.

Se o hook retorna funções ou objetos sem memoização, qualquer componente que use o hook e passe esses valores como props para filhos memoizados vai ter problemas — e o bug vai parecer inexplicável para quem não escreveu o hook.

JS
// ❌ Retorna uma função nova a cada render
function useToggle(initialValue) {
  const [value, setValue] = React.useState(initialValue)
 
  function toggle() {
    setValue(v => !v)
  }
 
  return [value, toggle]
}
 
// ✅ Retorna uma função estável
function useToggle(initialValue) {
  const [value, setValue] = React.useState(initialValue)
 
  const toggle = React.useCallback(() => {
    setValue(v => !v)
  }, []) // functional update, sem dependências
 
  return [value, toggle]
}

A versão com useCallback é um contrato melhor: quem consome o hook pode confiar que toggle vai ter a mesma referência entre renders, sem precisar saber dos detalhes internos do hook.

O React Compiler muda esse quadro?

Em outubro de 2025, o React lançou a versão 1.0 do React Compiler.

O que ele faz: analisa seu código em tempo de build e insere memoização automaticamente onde julga necessário. O output é JavaScript comum — não há novo runtime.

JSX
// Você escreve:
function Componente({ items }) {
  const ordenados = items.sort((a, b) => a.nome.localeCompare(b.nome))
  return <Lista data={ordenados} />
}
 
// O Compiler transforma em algo equivalente a:
function Componente({ items }) {
  const ordenados = React.useMemo(
    () => items.sort((a, b) => a.nome.localeCompare(b.nome)),
    [items]
  )
  return <Lista data={ordenados} />
}

Na prática, isso significa que, para projetos que adotam o Compiler, uma parte significativa dos casos onde você precisaria de useMemo e useCallback passa a ser tratada automaticamente.

Mas o Compiler não é uma solução completa:

O modelo correto de pensar no Compiler é: ele remove a necessidade de aplicar memoização manualmente na maioria dos casos. Mas entender o problema que useMemo e useCallback resolvem continua sendo fundamental — tanto para depurar quando algo re-renderiza errado, quanto para escrever código que funciona bem com ou sem o Compiler.

O que costuma confundir nesse assunto

1. Aplicar useMemo e useCallback em tudo por precaução

Não é uma otimização — é a adição de complexidade sem benefício real na maioria dos casos. Memoização tem custo: memória para guardar o valor anterior, comparação de dependências a cada render. Aplique em resposta a um problema identificado, não preventivamente.

2. Usar React.memo sem estabilizar as referências passadas como props

React.memo compara props com ===. Se você passa objetos, arrays ou funções criados inline no render do pai, o React.memo nunca vai "ativar" — ele sempre vai ver referências diferentes e re-renderizar.

3. Achar que useCallback com dependências vazias é sempre seguro

Dependências vazias significam que a função é criada uma vez e nunca recriada. Se ela ler algum valor de estado ou props, esse valor vai ficar "congelado" no closure. O resultado é um stale closure — bugs silenciosos que são difíceis de rastrear.

4. Confundir useMemo para cálculos com useMemo para referências

São dois usos distintos. O primeiro evita trabalho computacional desnecessário. O segundo preserva a identidade referencial de um valor para que React.memo funcione corretamente. Um não substitui o outro, e o segundo é geralmente mais comum na prática.

5. Esquecer que useCallback é useMemo para funções

São o mesmo mecanismo. useCallback(fn, deps) é equivalente a useMemo(() => fn, deps). Entender isso ajuda a pensar nos dois hooks de forma unificada em vez de aprender regras separadas para cada um.

Fechando a ideia

Se a gente resumir o modelo inteiro, ele fica assim:

Quando esse modelo fica claro, useMemo e useCallback deixam de parecer mágica ou superstição. Eles passam a ser ferramentas com um propósito preciso — e você consegue identificar quando aplicá-los, quando não aplicar, e por quê um componente está re-renderizando mais do que deveria.

← voltar para o blog