useMemo vs useCallback [ Simplest way explained ]

Ayan MasoodAyan Masood
3 min read

React's rendering behavior can sometimes be a performance bottleneck in your applications. Every time a component re-renders, React recreates almost everything inside it. Let me break down how React's memoization hooks can help solve this problem, and when you should use them.

The Re-rendering Problem

When a React component re-renders, here's what happens:

  • Every function gets redefined

  • Every constant gets recreated

  • Every object gets a new memory address

This means they all get new locations in memory, even if their values haven't changed. The only exceptions are values managed by useState and the effect functions in useEffect (though these still get re-mounted and unmounted according to their lifecycle).

By "redefined" or "reallocated," I mean these items get assigned new addresses in memory. This constant recreation can impact performance, especially in complex applications.

Enter Memoization: useMemo, useCallback, and memo()

React provides three powerful tools to optimize this behavior:

useCallback: Memoizing Functions

useCallback preserves function references between renders. Instead of creating a new function on each render, it returns the same function reference unless its dependencies change.

// Without useCallback - new function reference on every render
const handleClick = () => {
  console.log(count);
};

// With useCallback - same function reference between renders
const handleClick = useCallback(() => {
  console.log(count);
}, [count]); // Only changes when count changes

useMemo: Memoizing Values

useMemo caches the result of calculations between renders. It's perfect for expensive operations that shouldn't run on every render.

// Without useMemo - recalculates on every render
const expensiveValue = calculateSomethingExpensive(data);

// With useMemo - only recalculates when dependencies change
const expensiveValue = useMemo(() => {
  return calculateSomethingExpensive(data);
}, [data]); // Only recalculates when data changes

memo(): Memoizing Components

memo() prevents unnecessary re-renders of entire components. A memoized component only re-renders if its props actually change.

// Define a component
function MyComponent(props) {
  // Component logic
}

// Export a memoized version
export default memo(MyComponent);

The Tradeoff: Time vs. Space

Memoization is essentially a time-space tradeoff:

  • Benefit: Saves processing time by avoiding unnecessary recalculations and re-renders

  • Cost: Uses more memory to store the memoized values or functions

This is why memoizing everything is a bad practice. You're essentially trading RAM for CPU time, which isn't always beneficial.

When to Use Each Type of Memoization

Use useCallback when:

  • Passing functions to memoized child components

  • A function is a dependency in another hook

  • Functions are complex or create closures over many values

Use useMemo when:

  • Calculating values is computationally expensive

  • Derived values are used in multiple places in your component

  • You need to maintain referential equality for objects or arrays

Use memo() when:

  • Components render often but rarely need to update

  • Components have expensive rendering logic

  • You're rendering many instances of the same component (like in a list)

A Real-World Example

Imagine rendering a long list of items where only one item changes:

// Parent component
function TodoList({ todos, toggleTodo }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onToggle={useCallback(() => toggleTodo(todo.id), [todo.id, toggleTodo])}
        />
      ))}
    </ul>
  );
}

// Child component
const TodoItem = memo(function TodoItem({ todo, onToggle }) {
  console.log(`Rendering todo: ${todo.text}`);
  return (
    <li>
      <input 
        type="checkbox" 
        checked={todo.completed} 
        onChange={onToggle} 
      />
      {todo.text}
    </li>
  );
});

With this setup, when one todo changes, only that specific TodoItem component re-renders, not the entire list.

Conclusion

React's memoization hooks are powerful tools for performance optimization, but they should be used judiciously. Memoize components and calculations that are genuinely expensive or cause unnecessary re-renders, but DON’T overuse these tools for simple operations.

Remember that premature optimization can lead to more complex, harder-to-maintain code. Start with clean, readable code first, then optimize with memoization where you identify actual performance bottlenecks.

0
Subscribe to my newsletter

Read articles from Ayan Masood directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ayan Masood
Ayan Masood

Full Stack Web & App Developer, fostering tech communities, delivering talks at tech events while managing my own startup