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
- No primeiro render, o
useCallback
executa a função que passamos a ele e armazena essa instância. - 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
:
-
handleAddToCart
comuseCallback
:- 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.
- Esta função é memoizada e sua recriação depende exclusivamente da
-
Quando
toggleCatalogView
é chamado:- O estado
catalogView
doProductCatalog
muda, fazendo com queProductCatalog
renderize novamente (você verá “Rendering ProductCatalog…” no console). - Como
discountPercentage
não mudou,useCallback
retorna a mesma instância da funçãohandleAddToCart
que foi criada anteriormente. - Cada
ProductItem
, ao receber a proponAddToCart
, verá que a referência da função não mudou (graças aouseCallback
). - Como
ProductItem
é memoizado comReact.memo
e suas props (incluindoonAddToCart
) não mudaram de referência, os componentesProductItem
não irão renderizar novamente (você não verá “Rendering ProductItem…” no console para cada item).
- O estado
-
Quando
increaseDiscount
é chamado:- O estado
discountPercentage
doProductCatalog
muda, fazendo com queProductCatalog
renderize novamente. - Como
discountPercentage
mudou,useCallback
recria a funçãohandleAddToCart
para que ela capture o novo valor dediscountPercentage
em seu escopo. Uma nova referência parahandleAddToCart
é gerada. - Cada
ProductItem
agora recebe essa nova instância dehandleAddToCart
como proponAddToCart
. React.memo
emProductItem
detecta que a proponAddToCart
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.
- O estado