When to Memoize: useMemo vs. useCallback in React

React’s useMemo and useCallback hooks are powerful tools for optimizing performance, but they often lead to confusion. When should you use one over the other? Are they always necessary? And what exactly are they “memoizing”?

The Core Problem: unnecessary Renders

At the heart of useMemo and useCallback lies a common React performance bottleneck: unnecessary re-renders.

Every time a component’s state or props change, React re-renders that component and, by default, all of its children. This is usually fast, but if a child component is “expensive” (e.g., performs complex calculations, renders large lists, or frequently re-renders its own tree), these unnecessary updates can impact to performance.

Let say that you have a component that performs rendering on a complex list. If this component re-renders every time when its parent re-renders, even if the props relevant to that calculation or this list haven’t changed, you’re doing a lot of redundant work. This is where memoization comes in.

Memoization

Memoization is an optimization technique used to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

In React, useMemo and useCallback do exactly this. They takes two arguments: “create” function (returns function or value that you want to memoize) and dependency array (a list of values that the “create” function depends on).

useMemo is a Hook that lets you cache the result of a calculation between re-renders (Memoizing Values)

import { useState, useMemo } from 'react'

function ProductList({ products, filterText }) {
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...')
    return products.filter(product =>
      product.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [products, filterText])

  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

function App() {
  const [searchText, setSearchText] = useState('')
  const allProducts = [
    { id: 1, name: 'Laptop' },
    { id: 2, name: 'Mouse' },
    { id: 3, name: 'Keyboard' },
    { id: 4, name: 'Monitor' },
  ]

  return (
    <div>
      <input
        type='text'
        value={searchText}
        onChange={(e) => setSearchText(e.target.value)}
        placeholder='Search products'
      />
      <ProductList products={allProducts} filterText={searchText} />
    </div>
  )
}
export default App

useCallback is a Hook that lets you cache a function definition between re-renders (Memoizing Functions)

import React, { useState, useCallback } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  // Without useCallback, handleIncrement and handleDecrement would be a new function on every render
  const handleIncrement = useCallback(() => {
    setCount(prevCount => prevCount + 1)
  }, []) // Empty dependency array means this function never changes

  const handleDecrement = useCallback(() => {
    setCount(prevCount => prevCount - 1)
  }, [])

  console.log('Component rendered')

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
}
export default Counter

When to Choose Which

  • useMemo for values, useCallback for functions.
  • Choose useMemo when you want to avoid re-calculating an expensive value. Choose useCallback when you want to avoid re-creating a function instance, primarily to prevent unnecessary re-renders of memoized child components or to optimize useEffect dependencies.

Important Considerations and Potential Pitfalls

  • Don’t Overuse Them: Memoization comes with its own overhead. React needs to store the cached values/functions and compare dependencies. If the calculation or function recreation is very cheap, the overhead of useMemo/useCallback might outweigh the benefits. Profile your application to identify actual performance bottlenecks before aggressively applying these hooks.
  • Correct Dependency Arrays: This is crucial. If you omit a dependency or provide an incorrect one, your memoized value or function might become “stale,” leading to bugs. Always ensure your dependency array accurately reflects all values from the component’s scope that are used inside the memoized function or value calculation. ESLint’s exhaustive-deps rule is your friend here!
  • Referential Equality: Remember that useMemo and useCallback work based on referential equality. If you’re comparing objects or arrays in your dependencies, a new object/array literal will always be considered different, even if its contents are identical.

Reference

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top