useMemo and useCallback
Optimize performance with memoization — and know when not to use it
Memoization caches a value so it's only recomputed when dependencies change. useMemo caches computed values; useCallback caches function references.
useMemo: Expensive Computations
useMemo caches the result of a computation between renders.
"use client"
import { useState, useMemo } from "react"
export default function SearchList() {
const [query, setQuery] = useState("")
const [items] = useState(() =>
Array.from(
{ length: 10000 },
(_, i) => `Item ${i}: ${Math.random().toString(36).slice(2)}`
)
)
// Without useMemo: filtered runs on every keystroke
// With useMemo: filtered only runs when query or items change
const filtered = useMemo(() => {
console.log("Filtering...")
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
)
}, [query, items])
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{filtered.slice(0, 20).map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</div>
)
}
useMemo adds memory overhead (cached result + dependency tracking). For
trivial computations like a + b, the cache check costs more than
recomputing. Profile first — if the computation is under 1ms, skip the memo.
useCallback: Stable Function References
useCallback returns the same function instance between renders unless dependencies change.
"use client"
import { useState, useCallback } from "react"
export default function CallbackDemo() {
const [count, setCount] = useState(0)
// New function every render without useCallback
const handleClick = useCallback(() => {
setCount((c) => c + 1)
}, []) // empty deps — setCount is stable
return <SlowButton onClick={handleClick} label={`Count: ${count}`} />
}
// React.memo: only re-render if props change
const SlowButton = React.memo(function SlowButton({
onClick,
label,
}: {
onClick: () => void
label: string
}) {
console.log("SlowButton rendered")
return <button onClick={onClick}>{label}</button>
})
useCallback + React.memo
useCallback is most valuable when paired with React.memo. Without useCallback, a parent re-render creates a new function reference, causing React.memo to see a changed prop and re-render the child regardless.
When Memoization Hurts
Memoization is not free. Every useMemo/useCallback call does:
- Allocates the dependency array on render
- Compares previous deps with current deps (
Object.ison each) - Either returns cached value (fast) or recomputes (slow path)
Memoization wins when:
- Computation is expensive (arrays > 10k items, complex math, deep object creation)
- Referential stability prevents cascading child re-renders
Memoization loses when:
- Computation is cheap (string concat, simple arithmetic, boolean logic)
- Dependencies change on every render anyway (memoized value is never reused)
- Component tree is shallow (no memoized children to benefit)
Open your browser DevTools > React Profiler. Record interactions. If a component re-renders without prop changes, that's where useCallback/useMemo helps. If no unnecessary re-renders, adding memoization is pure overhead.
Real Example: Filterable Product List
Combine useMemo and useCallback to optimize a product list with filtering, sorting, and pagination.
"use client"
import { useState, useMemo, useCallback } from "react"
type Product = { id: number; name: string; price: number; category: string }
export default function ProductList({ products }: { products: Product[] }) {
const [category, setCategory] = useState("all")
const [sortBy, setSortBy] = useState<"name" | "price">("name")
// Memoize the filtered product list
const visible = useMemo(() => {
const filtered =
category === "all"
? products
: products.filter((p) => p.category === category)
return [...filtered].sort((a, b) =>
sortBy === "name" ? a.name.localeCompare(b.name) : a.price - b.price
)
}, [products, category, sortBy])
// Stabilize event handlers
const onCategoryChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => setCategory(e.target.value),
[]
)
const onSortChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) =>
setSortBy(e.target.value as "name" | "price"),
[]
)
return (
<div>
<select value={category} onChange={onCategoryChange}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<select value={sortBy} onChange={onSortChange}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ProductGrid products={visible} />
</div>
)
}
const ProductGrid = React.memo(function ProductGrid({
products,
}: {
products: Product[]
}) {
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — ${p.price}
</li>
))}
</ul>
)
})
See It In Action
Optimize a list component that renders search results. The list receives a filter function and displays 1000 items. Use useMemo for the filtered list and useCallback for the filter handler.
1"use client"2 3import { useState, useMemo, useCallback, memo } from "react"4 5const ITEMS = Array.from({ length: 1000 }, (_, i) => ({6id: i,7title: `Item ${i}: ${["Apple", "Banana", "Cherry", "Date", "Elderberry"][i % 5]}`,8description: `Description for item ${i}`9}))10 11export default function SearchPage() {12const [query, setQuery] = useState("")13 14const filtered = useMemo(() => {15if (!query.trim()) return ITEMS16const lower = query.toLowerCase()17return ITEMS.filter(18(item) =>19item.title.toLowerCase().includes(lower) ||20item.description.toLowerCase().includes(lower)21)22}, [query])23 24const handleQueryChange = useCallback(25(e: React.ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),26[]27)28 29return (30 31<div>32<input33 value={query}34 onChange={handleQueryChange}35 placeholder="Search items..."36 />37<ResultList items={filtered} />38</div>39)40}41 42const ResultList = memo(function ResultList({43items,44}: {45items: typeof ITEMS46}) {47console.log("ResultList rendered")48return (49 50<ul>51{items.slice(0, 50).map((item) => (52<li key={item.id}>{item.title}</li>53))}54</ul>55)56})useMemo filters 1000 items by search query on every keystroke. useCallback stabilizes the onChange handler so React.memo on ResultList can skip re-renders when query hasn't changed.