I still remember the dashboard project where useMemo and useCallback went from "nice-to-have" to "saved my bacon." We had a product grid with 200+ items, live filtering, price calculations with taxes/discounts, and sorting. Every keystroke in the filter box was triggering 18,000+ re-renders per second. Chrome DevTools was screaming, users were complaining about lag, and our client was asking why the "simple React app" felt slower than their old jQuery site.
That's when I learned useMemo and useCallback aren't just performance trivia—they're survival tools for real apps. Let me walk you through exactly how I fixed that disaster and the patterns I use in every React project since.
The product grid catastrophe
The Problem: A dashboard showing 200 products with:
- Live search by name/category
- Price calculation (base + tax - discount)
- Sorting by price, name, rating
- Each row had image lazy-loading + hover effects
function ProductGrid({ products, searchTerm, sortBy }) {
// This filtered 200 items on EVERY keystroke
const filtered = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// This sorted 200 items on EVERY keystroke
const sorted = filtered.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
return a.name.localeCompare(b.name);
});
// This calculated final price for 200 items EVERY TIME
const displayProducts = sorted.map(product => ({
...product,
finalPrice: product.price * 1.18 - (product.discount || 0)
}));
return (
<div className="grid">
{displayProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onQuickAdd={handleQuickAdd} // NEW FUNCTION EVERY RENDER
/>
))}
</div>
);
}
useMemo: Memoizing expensive values
function ProductGrid({ products, searchTerm, sortBy }) {
const filteredProducts = useMemo(() => {
let result = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.category.toLowerCase().includes(searchTerm.toLowerCase())
);
// Sort only once per keystroke
if (sortBy === 'price') {
result.sort((a, b) => a.finalPrice - b.finalPrice);
} else if (sortBy === 'name') {
result.sort((a, b) => a.name.localeCompare(b.name));
}
return result;
}, [products, searchTerm, sortBy]); // Only re-run when THESE change
// Rest of component...
}
function ProductCard({ product, onQuickAdd }) {
const finalPrice = useMemo(() => {
return Math.round(product.price * 1.18 - (product.discountAmount || 0));
}, [product.price, product.discountAmount]);
const discountPercent = useMemo(() => {
if (!product.originalPrice) return 0;
return Math.round((product.originalPrice - product.price) / product.originalPrice * 100);
}, [product.originalPrice, product.price]);
return (
<div className="card">
<h3>{product.name}</h3>
<div className="price">
₹{finalPrice}
{discountPercent > 0 && <span>-{discountPercent}%</span>}
</div>
<button onClick={() => onQuickAdd(product.id)}>Quick Add</button>
</div>
);
}
useCallback: Stabilizing function references
function ProductGrid({ products, searchTerm }) {
// These created NEW FUNCTIONS every render!
const handleQuickAdd = (productId) => {
addToCart(productId);
};
const handleFavorite = (productId) => {
toggleFavorite(productId);
};
return products.map(product => (
<ProductCard
key={product.id}
product={product}
onQuickAdd={handleQuickAdd} // NEW every render
onFavorite={handleFavorite} // NEW every render
/>
));
}
function ProductGrid({ products, searchTerm }) {
const handleQuickAdd = useCallback((productId) => {
addToCart(productId);
showToast(`Added ${productId} to cart!`);
}, []); // Empty deps = stable function forever
const handleFavorite = useCallback((productId) => {
toggleFavorite(productId);
}, []); // Dependencies go here if needed
return products.map(product => (
<ProductCard
product={product}
onQuickAdd={handleQuickAdd} // SAME function reference
onFavorite={handleFavorite} // SAME function reference
/>
));
}
Real performance before/after (measured)
My decision framework (5+ years React experience)
- Expensive calculations (>100ms)
- Filtering/sorting large lists (>50 items)
- Complex object transformations
- Values used multiple times in render
// Product search results (200+ items)
const searchResults = useMemo(() =>
products.filter(p => matchesSearch(p, query)),
[products, query]
);
// Chart data transformation
const chartData = useMemo(() => transformSalesData(rawData), [rawData]);
// Pagination calculations
const pageItems = useMemo(() =>
items.slice((page-1)*pageSize, page*pageSize),
[items, page, pageSize]
);
- Passing functions to React.memo components
- Functions with inline dependencies
- Event handlers in lists (>10 items)
- Custom hooks returning functions
const handleRowClick = useCallback((rowId) => {
setSelectedRow(rowId);
loadRowDetails(rowId);
}, []);
const debouncedSearch = useCallback(
debounce((query) => fetchResults(query), 300),
[]
);
Common mistakes I made (and fixed)
// WRONG: Premature optimization
const simpleValue = useMemo(() => a + b, [a, b]); // 2ms → 2.1ms
// RIGHT: Only expensive stuff
const expensiveValue = useMemo(() => heavyCalculation(a, b), [a, b]);
// WRONG: Stale closure bug
const handleUpdate = useCallback(() => {
console.log(user.name); // Always shows old name!
}, []);
const handleUpdate = useCallback(() => {
console.log(user.name); // Always current!
}, [user.name]);
// WRONG: Creates new object every time deps change
const config = useMemo(() => ({ limit: 10, sort: 'asc' }), [limit]);
// RIGHT: Stable reference or useCallback for functions
const getConfig = useCallback(() => ({ limit, sort }), [limit, sort]);
Complete optimized ProductGrid (production ready)
const ProductGrid = React.memo(({ products, filters }) => {
const searchResults = useMemo(() => {
return products
.filter(p => matchesFilters(p, filters))
.sort((a, b) => compareProducts(a, b, filters.sortBy));
}, [products, filters]);
const handleQuickAdd = useCallback((productId) => {
addToCart(productId);
}, []);
return (
<div className="grid">
{searchResults.map(product => (
<ProductCard
key={product.id}
product={product}
onQuickAdd={handleQuickAdd}
/>
))}
</div>
);
});
When NOT to use memo hooks
- Simple calculations (<1ms)
- Single renders (no child components)
- Learning/prototyping phase
- Team unfamiliar with React.memo patterns
Conclusion: My React optimization philosophy
Key Differences Between useMemo and useCallback
