Skip
Arish's avatar

12. useMemo and useCallback


useMemo and useCallback

These hooks help optimize performance by memoizing values and functions to prevent unnecessary recalculations and re-renders.

useMemo

Memoizes a computed value:

jsx
1import { useMemo } from 'react'
2
3function ExpensiveComponent({ items, filter }) {
4  // Without useMemo - runs on every render
5  const filteredItems = items.filter(item => item.includes(filter))
6  
7  // With useMemo - only recalculates when items or filter change
8  const filteredItems = useMemo(() => {
9    return items.filter(item => item.includes(filter))
10  }, [items, filter])
11  
12  return <List items={filteredItems} />
13}

When to Use useMemo

jsx
1// ✅ Good: Expensive calculations
2const sortedData = useMemo(() => {
3  return [...data].sort((a, b) => a.name.localeCompare(b.name))
4}, [data])
5
6// ✅ Good: Creating objects to pass as props
7const style = useMemo(() => ({
8  color: theme === 'dark' ? 'white' : 'black',
9  fontSize: size * 2
10}), [theme, size])
11
12// ✅ Good: Complex filtering/transformation
13const visibleTodos = useMemo(() => {
14  return todos
15    .filter(todo => !todo.completed || showCompleted)
16    .sort((a, b) => b.priority - a.priority)
17    .slice(0, limit)
18}, [todos, showCompleted, limit])
19
20// ❌ Bad: Simple values (overhead not worth it)
21const doubled = useMemo(() => count * 2, [count])

useMemo Examples

jsx
1function SearchResults({ query, allItems }) {
2  // Expensive search operation
3  const results = useMemo(() => {
4    console.log('Searching...')
5    return allItems.filter(item => {
6      // Complex matching logic
7      return item.title.toLowerCase().includes(query.toLowerCase()) ||
8             item.tags.some(tag => tag.includes(query))
9    })
10  }, [query, allItems])
11  
12  // Even more expensive: sorting results
13  const sortedResults = useMemo(() => {
14    return [...results].sort((a, b) => b.relevance - a.relevance)
15  }, [results])
16  
17  return (
18    <ul>
19      {sortedResults.map(item => (
20        <li key={item.id}>{item.title}</li>
21      ))}
22    </ul>
23  )
24}

useCallback

Memoizes a function:

jsx
1import { useCallback } from 'react'
2
3function ParentComponent() {
4  const [count, setCount] = useState(0)
5  
6  // Without useCallback - new function every render
7  const handleClick = () => {
8    console.log('Clicked')
9  }
10  
11  // With useCallback - same function reference
12  const handleClick = useCallback(() => {
13    console.log('Clicked')
14  }, [])
15  
16  return <ChildComponent onClick={handleClick} />
17}

Why useCallback Matters

jsx
1// Child component wrapped in React.memo
2const ExpensiveChild = React.memo(function ExpensiveChild({ onClick }) {
3  console.log('Child rendered')
4  return <button onClick={onClick}>Click me</button>
5})
6
7function Parent() {
8  const [count, setCount] = useState(0)
9  
10  // ❌ Without useCallback: Child re-renders every time Parent renders
11  const handleClick = () => console.log('click')
12  
13  // ✅ With useCallback: Child only re-renders when dependencies change
14  const handleClick = useCallback(() => console.log('click'), [])
15  
16  return (
17    <div>
18      <p>Count: {count}</p>
19      <button onClick={() => setCount(c => c + 1)}>Increment</button>
20      <ExpensiveChild onClick={handleClick} />
21    </div>
22  )
23}

useCallback with Dependencies

jsx
1function TodoList({ todos, onToggle }) {
2  // Depends on onToggle prop
3  const handleToggle = useCallback((id) => {
4    onToggle(id)
5  }, [onToggle])
6  
7  return (
8    <ul>
9      {todos.map(todo => (
10        <TodoItem 
11          key={todo.id} 
12          todo={todo} 
13          onToggle={handleToggle} 
14        />
15      ))}
16    </ul>
17  )
18}
19
20// Using state in callback
21function Counter() {
22  const [count, setCount] = useState(0)
23  
24  // ❌ This captures stale count
25  const logCount = useCallback(() => {
26    console.log(count)  // Always logs initial count
27  }, [])
28  
29  // ✅ Include count in dependencies
30  const logCount = useCallback(() => {
31    console.log(count)
32  }, [count])
33  
34  // ✅ Or use functional update to avoid dependency
35  const increment = useCallback(() => {
36    setCount(c => c + 1)  // No dependency on count
37  }, [])
38}

useCallback vs useMemo

jsx
1// useCallback returns a function
2const memoizedFn = useCallback(() => {
3  doSomething(a, b)
4}, [a, b])
5
6// useMemo returns a value (can be a function)
7const memoizedFn = useMemo(() => {
8  return () => doSomething(a, b)
9}, [a, b])
10
11// These are equivalent!
12useCallback(fn, deps) === useMemo(() => fn, deps)

Combined Usage

jsx
1function ProductList({ products, sortBy, filterBy }) {
2  // Memoize filtered and sorted data
3  const displayProducts = useMemo(() => {
4    let result = products.filter(p => p.category === filterBy)
5    
6    if (sortBy === 'price') {
7      result = [...result].sort((a, b) => a.price - b.price)
8    } else if (sortBy === 'name') {
9      result = [...result].sort((a, b) => a.name.localeCompare(b.name))
10    }
11    
12    return result
13  }, [products, sortBy, filterBy])
14  
15  // Memoize event handlers
16  const handleAddToCart = useCallback((productId) => {
17    addToCart(productId)
18  }, [])
19  
20  const handleFavorite = useCallback((productId) => {
21    toggleFavorite(productId)
22  }, [])
23  
24  return (
25    <div>
26      {displayProducts.map(product => (
27        <ProductCard
28          key={product.id}
29          product={product}
30          onAddToCart={handleAddToCart}
31          onFavorite={handleFavorite}
32        />
33      ))}
34    </div>
35  )
36}

When NOT to Use

jsx
1// ❌ Premature optimization - simple operations
2const doubled = useMemo(() => count * 2, [count])
3
4// ❌ Functions that change every render anyway
5const handleSubmit = useCallback(() => {
6  submitForm(formData)  // formData changes often
7}, [formData])
8
9// ❌ When child isn't memoized
10function Parent() {
11  const onClick = useCallback(() => {}, [])
12  return <Child onClick={onClick} />  // Child will re-render anyway!
13}

Performance Guidelines

  1. Measure first - Don't optimize without profiling
  2. Use with React.memo - useCallback alone doesn't prevent re-renders
  3. Keep dependencies minimal - More deps = more recalculations
  4. Consider the cost - Memoization has its own overhead
jsx
1// Complete optimization pattern
2const MemoizedChild = React.memo(function Child({ data, onAction }) {
3  return <div onClick={() => onAction(data.id)}>{data.name}</div>
4})
5
6function Parent({ items }) {
7  const [selected, setSelected] = useState(null)
8  
9  // Memoize processed data
10  const processedItems = useMemo(() => 
11    items.map(item => ({ ...item, formatted: format(item) })),
12    [items]
13  )
14  
15  // Memoize handler
16  const handleAction = useCallback((id) => {
17    setSelected(id)
18  }, [])
19  
20  return (
21    <div>
22      {processedItems.map(item => (
23        <MemoizedChild 
24          key={item.id}
25          data={item}
26          onAction={handleAction}
27        />
28      ))}
29    </div>
30  )
31}

These hooks are powerful tools for React performance optimization!