useCallback

May 27, 2025

O que é useCallback?

Desenvolvendo usando React, eu tenho me deparado com a necessidade de otimizar o desempenho de alguns componentes. O useCallback é um dos Hooks do React que pode servir justamente para esse propósito, ele “memoriza” a definição de uma função entre os re-renders de um componente.

Em componentes, toda vez que um estado ou props mudam e o componente renderiza novamente, as funções declaradas dentro dele são, por padrão, recriadas também. O useCallback entra nesse processo, ele fornece uma versão memoizada da função. Essa versão só é alterada se alguma das dependências que foram especificadas foram modificadas.

Como o useCallback funciona?

A ideia central do useCallback é a memoização da definição da função, não do seu resultado

  1. No primeiro render, o useCallback executa a função que passamos a ele e armazena essa instância.
  2. Nos reders seguintes, ele verifica o array de dependências.
    • Se nenhuma dependência mudou, ele retorna a instância da função que já estava memoizada.
    • Se alguma dependência mudou, ele recria a função, memoiza essa nova instância e retorna ela.

O array de dependências é o segundo argumeto do useCallback e é crucial. Ele deve ter todos os valores (props, estados, etc.) que a função utiliza.

  • Array vazio ([]): A função é criada uma única vez na montagem do componente e a mesma instância é retornada sempre.
  • Sem array: O useCallback nao memoiza nada; a função é recriada a cara renderização.
import React, { useState, useCallback } from "react";

const initialProducts = [
  { id: "p1", name: "Awesome T-Shirt", price: 20 },
  { id: "p2", name: "Cool Mug", price: 12 },
  { id: "p3", name: "Fancy Hat", price: 25 },
];

const ProductItem = React.memo(({ product, onAddToCart }) => {
  console.log(`Rendering ProductItem: ${product.name}`);
  return (
    <div
      style={{
        border: "1px solid #ccc",
        margin: "8px",
        padding: "8px",
        borderRadius: "4px",
      }}
    >
      <h4>{product.name}</h4>
      <p>Price: ${product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
    </div>
  );
});

function ProductCatalog() {
  const [products, setProducts] = useState(initialProducts);
  const [cartItemCount, setCartItemCount] = useState(0);
  const [discountPercentage, setDiscountPercentage] = useState(0);

  // Este estado serve apenas para acionar re-renderizações no ProductCatalog
  // para demonstração, sem afetar as dependências do callback.
  const [catalogView, setCatalogView] = useState("grid");

  // A função handleAddToCart é memoizada usando useCallback.
  // Ela SÓ será recriada se 'discountPercentage' mudar.
  const handleAddToCart = useCallback(
    (productId) => {
      console.log(
        `Adding product ${productId} to cart. Current discount: ${discountPercentage}%`,
      );
      // Em uma aplicação real, isso atualizaria o estado do carrinho, possivelmente usando detalhes do produto e o desconto.
      // Para este exemplo, vamos apenas incrementar um contador.
      setCartItemCount((prevCount) => prevCount + 1);
    },
    [discountPercentage],
  ); // Dependência: 'discountPercentage'
  // Se 'discountPercentage' mudar, uma nova 'handleAddToCart' é criada.
  // `setCartItemCount` é estável e não precisa ser uma dependência.

  const toggleCatalogView = () => {
    setCatalogView((prevView) => (prevView === "grid" ? "list" : "grid"));
  };

  const increaseDiscount = () => {
    setDiscountPercentage((prevDiscount) => prevDiscount + 5);
  };

  console.log("Rendering ProductCatalog...");

  return (
    <div>
      <h2>Our Products (View: {catalogView})</h2>
      <p>Items in cart: {cartItemCount}</p>
      <p>Current Discount: {discountPercentage}%</p>

      <button onClick={toggleCatalogView} style={{ marginRight: "10px" }}>
        Toggle Catalog View (Parent re-render, child ideally not)
      </button>
      <button onClick={increaseDiscount}>
        Increase Discount (Parent re-render, callback changes, child re-renders)
      </button>

      <div
        style={{ marginTop: "20px", border: "1px solid #eee", padding: "10px" }}
      >
        <h3>Product List:</h3>
        {products.map((product) => (
          <ProductItem
            key={product.id}
            product={product}
            onAddToCart={handleAddToCart}
          />
        ))}
      </div>
    </div>
  );
}

export default ProductCatalog;

Nesse exemplo, o componente ProductItem é memoizado usando React.memo. A função handleAddToCart é definida no ProductCatalog e passada como prop onAddToCart para ProductItem:

  1. handleAddToCart com useCallback:

    • Esta função é memoizada e sua recriação depende exclusivamente da discountPercentage.
    • A função setCartItemCount tem garantia de ser estável pelo React e não precisa ser listada como dependência se estivermos usando a forma funcional de atualização de estado (prevCount => prevCount + 1), como é o caso.
  2. Quando toggleCatalogView é chamado:

    • O estado catalogView do ProductCatalog muda, fazendo com que ProductCatalog renderize novamente (você verá “Rendering ProductCatalog…” no console).
    • Como discountPercentage não mudou, useCallback retorna a mesma instância da função handleAddToCart que foi criada anteriormente.
    • Cada ProductItem, ao receber a prop onAddToCart, verá que a referência da função não mudou (graças ao useCallback).
    • Como ProductItem é memoizado com React.memo e suas props (incluindo onAddToCart) não mudaram de referência, os componentes ProductItem não irão renderizar novamente (você não verá “Rendering ProductItem…” no console para cada item).
  3. Quando increaseDiscount é chamado:

    • O estado discountPercentage do ProductCatalog muda, fazendo com que ProductCatalog renderize novamente.
    • Como discountPercentage mudou, useCallback recria a função handleAddToCart para que ela capture o novo valor de discountPercentage em seu escopo. Uma nova referência para handleAddToCart é gerada.
    • Cada ProductItem agora recebe essa nova instância de handleAddToCart como prop onAddToCart.
    • React.memo em ProductItem detecta que a prop onAddToCart mudou de referência.
    • Consequentemente, todos os componentes ProductItem irão renderizar novamente (você verá “Rendering ProductItem…” para cada um), mesmo que os dados do produto em si não tenham mudado. Isso é esperado, pois a função que eles usam para uma ação crítica foi, de fato, atualizada.