Back to blogs

Understanding the useMemo and useCallback hook in React

A guide that explains how to use useMemo and useCallback hook when to used it and not to used it.

9 min read
By Benjo Quilario

What are React Hooks?

Hooks were added to React in version 16.8.

Hooks allow function components to have access to state and other React features. Because of this, class components are generally no longer needed.

Hooks make your work a lot easier and help you avoid repetitive code.

We're not going to discuss all React hooks, but we're going to understand what useMemo and useCallback hooks are and when to use them.

The useMemo Hook

The useMemo hook is used to memoize a value that is expensive to compute. This means that the value is only computed when one of its dependencies changes. This can be useful for optimizing the performance of a component that needs to perform heavy computations each time it renders.

We really need to understand the useMemo hook and how it works and when to use it, because if you don't use it when you should, it can trigger performance problems and might cause bugs or unexpected behavior.

First, let's talk about the problem of not using useMemo.

Take a look at this code and analyze it.

import { useState } from "react"

const initialItems = new Array(29_999_999)
  .fill(0)
  .map((_, index) => ({ id: index, isSelected: index === 29_999_998 }))

const Component = () => {
  const [count, setCount] = useState(0)
  const [items] = useState(initialItems)

  const selectedItem = items.find((item) => item.isSelected)

  return (
    <div>
      <h1>Count: {count}</h1>
      <h2>Selected Item: {selectedItem?.id}</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

As you can see in the code above, we import useState from React and create initialItems with an array of 29,999,999 elements filled with 0, then we map over it and return an object with id and isSelected properties. The isSelected property is a boolean value that will only be true when the index equals 29,999,998 (the last item).

It looks normal - nothing seems wrong. We declare useState, pass initialItems as the initial state, and have count and setCount with an initial value of 0. But if you run this code in a React application, it will cause problems. Try it in your application and try to "spam" the increment button - you'll notice the Count is skipping numbers.

This is wrong and not how a performant application should behave. So what's the problem?

We need to look at our component and understand how React works under the hood, how React treats state updates, and how it renders components.

In our component, we have count, items, and selectedItem. When we click the button, all we're doing is calling the function in onClick and incrementing the count.

We need to understand that updating state means you have to trigger a re-render of the entire component.

The problem is that selectedItem is triggering our performance issues. If you look at what it does: it takes the items array and goes through the entire array to find the item where isSelected is true. But if we look at initialItems, the only item that has isSelected set to true is the last one, so it has to go through 29 million items before finding the selected item and returning that value. This is a very expensive operation.

Because we are changing the count, we are triggering the component to re-render and causing our selectedItem to be recalculated every time the count changes. This causes a huge performance issue - we're doing unnecessary computations on every render.

This is where useMemo comes into play.

Do we really need to use useMemo on items? If the items array stays the same, then no, because it remains the same no matter what happens. But if we look at our component, selectedItem is being recalculated unnecessarily.

import { useState, useMemo } from "react"

const initialItems = new Array(29_999_999)
  .fill(0)
  .map((_, index) => ({ id: index, isSelected: index === 29_999_998 }))

const Component = () => {
  const [count, setCount] = useState(0)
  const [items] = useState(initialItems)

  const selectedItem = useMemo(
    () => items.find((item) => item.isSelected),
    [items]
  )

  return (
    <div>
      <h1>Count: {count}</h1>
      <h2>Selected Item: {selectedItem?.id}</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

Now, selectedItem will only be recalculated if the items dependency in the useMemo dependency array changes.

Go back to your application and try to spam the increment button. You'll see the difference between the two approaches - when we're not using the useMemo hook, there's lag and skipping numbers. With useMemo, there's no lag, no skipping numbers, and it's super efficient.

The useCallback Hook

The useCallback hook is used to memoize a function. This means that the function is only recreated when one of its dependencies changes. This can be useful for optimizing the performance of a component that passes functions as props to child components.

Let's first understand the problem of not using useCallback.

The Problem: Function Recreation on Every Render

Take a look at this code and analyze it:

import { useState } from "react"

const ExpensiveChildComponent = ({ onClick, items }) => {
  console.log("ExpensiveChildComponent rendered")

  // Simulate expensive computation
  const processedItems = items.map((item) => ({
    ...item,
    processed: true,
    timestamp: Date.now(),
  }))

  return (
    <div>
      <h3>Processed Items: {processedItems.length}</h3>
      <button onClick={onClick}>Click me</button>
    </div>
  )
}

const ParentComponent = () => {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: "Item 1" },
    { id: 2, name: "Item 2" },
    { id: 3, name: "Item 3" },
  ])

  // This function is recreated on every render
  const handleClick = () => {
    console.log("Button clicked!")
  }

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveChildComponent onClick={handleClick} items={items} />
    </div>
  )
}

In this example, every time the count state changes and the ParentComponent re-renders, a new handleClick function is created. Even though the function does exactly the same thing, React sees it as a different function because it's a new reference.

This causes the ExpensiveChildComponent to re-render unnecessarily because it receives a "new" onClick prop every time, even though the function's behavior hasn't changed.

You'll notice in the console that "ExpensiveChildComponent rendered" is logged every time you click the increment button, even though the items prop hasn't changed and the onClick function does the same thing.

The Solution: Using useCallback

import { useState, useCallback } from "react"

const ExpensiveChildComponent = ({ onClick, items }) => {
  console.log("ExpensiveChildComponent rendered")

  // Simulate expensive computation
  const processedItems = items.map((item) => ({
    ...item,
    processed: true,
    timestamp: Date.now(),
  }))

  return (
    <div>
      <h3>Processed Items: {processedItems.length}</h3>
      <button onClick={onClick}>Click me</button>
    </div>
  )
}

const ParentComponent = () => {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: "Item 1" },
    { id: 2, name: "Item 2" },
    { id: 3, name: "Item 3" },
  ])

  // This function is memoized and only recreated if dependencies change
  const handleClick = useCallback(() => {
    console.log("Button clicked!")
  }, []) // Empty dependency array means this function never changes

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveChildComponent onClick={handleClick} items={items} />
    </div>
  )
}

Now, handleClick is memoized using useCallback. Since the dependency array is empty [], the function reference never changes. This means ExpensiveChildComponent won't re-render when the count changes, because its props (onClick and items) remain the same.

When useCallback Dependencies Matter

Here's an example where the callback depends on state:

import { useState, useCallback } from "react"

const TodoList = ({ todos, onToggle }) => {
  console.log("TodoList rendered")

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => onToggle(todo.id)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

const TodoApp = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: "Learn React", completed: false },
    { id: 2, text: "Master useCallback", completed: false },
  ])
  const [filter, setFilter] = useState("all")

  // This callback depends on the current todos state
  const handleToggle = useCallback((id) => {
    setTodos((currentTodos) =>
      currentTodos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }, []) // We can use empty deps because we use the functional update form

  const filteredTodos = todos.filter((todo) => {
    if (filter === "completed") return todo.completed
    if (filter === "active") return !todo.completed
    return true
  })

  return (
    <div>
      <div>
        <button onClick={() => setFilter("all")}>All</button>
        <button onClick={() => setFilter("active")}>Active</button>
        <button onClick={() => setFilter("completed")}>Completed</button>
      </div>
      <TodoList todos={filteredTodos} onToggle={handleToggle} />
    </div>
  )
}

In this example, even when the filter changes and causes a re-render, the TodoList component won't re-render unnecessarily because the handleToggle function reference remains stable.

Key Points About useCallback:

  1. Use it when passing functions as props to child components that are expensive to render
  2. The dependency array matters - include any values from component scope that the function uses
  3. Don't overuse it - it has its own overhead, so only use it when you have a performance problem
  4. Combine with React.memo on child components for maximum effectiveness
import React from "react"

const ExpensiveChildComponent = React.memo(({ onClick, items }) => {
  console.log("ExpensiveChildComponent rendered")

  // Expensive computation here...

  return <div>{/* Component JSX */}</div>
})

By wrapping the child component with React.memo, it will only re-render when its props actually change, making the useCallback optimization effective.

Conclusion

Understanding useMemo and useCallback is crucial for building performant React applications. Here are the key takeaways:

When to use useMemo:

  • ✅ For expensive calculations that depend on specific values
  • ✅ When creating objects or arrays that are passed as props
  • ✅ When the computation is truly expensive (like processing large datasets)
  • ❌ Don't use it for simple calculations or primitive values
  • ❌ Don't use it everywhere - it has its own overhead

When to use useCallback:

  • ✅ When passing functions as props to child components
  • ✅ When the child component is wrapped with React.memo
  • ✅ When functions are dependencies of other hooks
  • ❌ Don't use it for functions that don't get passed as props
  • ❌ Don't use it if the dependencies change frequently

Best Practices:

  1. Profile first - Use React DevTools Profiler to identify actual performance bottlenecks
  2. Measure the impact - Don't optimize prematurely
  3. Combine optimizations - Use React.memo with useCallback for maximum effectiveness
  4. Keep dependency arrays accurate - Include all values from component scope that are used

Remember, these hooks are optimization tools. Use them when you have identified actual performance problems, not as a default solution for every function or calculation in your components.

Related Posts