Back to Blog
Lesson 5 of the Advanced React: Performance, Architecture & Patterns course
ReactJune 27, 20264 min read

Mastering useCallback and useMemo for React Performance

Stop unnecessary re-renders by mastering useCallback and useMemo. Learn to stabilize references and optimize dependency arrays to keep your React app fast.

ReactPerformanceuseCallbackuseMemoHooksjavascriptfrontend

Previously in this course, we explored Strategic use of React.memo to prevent child components from re-rendering when their props haven't changed. However, memoization is only half the battle. If your component passes functions or objects as props, those props might be re-created on every render, breaking the shallow comparison that React.memo relies on.

In this lesson, we master useCallback and useMemo to enforce Referential Equality, ensuring our memoized components stay performant instead of constantly re-rendering.

The Core Problem: Referential Equality

In JavaScript, objects and functions are compared by reference, not by value. When you define a function inside a component body, it is a new function instance every time the component renders.

JAVASCRIPT
// This function is created anew on every render
const handleClick = () => console.log(CE9178">'Clicked!');

Even if handleClick has the same logic as the previous render, prevProps.handleClick === nextProps.handleClick will be false. If you pass this function to a child wrapped in React.memo, that child will re-render every single time the parent does. This is the "unstable reference" trap.

Comparing the Hooks

HookPurposeReturn Value
useMemoCache the result of a calculationThe calculated value
useCallbackCache the function instance itselfThe memoized function

Stabilizing References with useCallback

Use useCallback to prevent a function from being recreated unless its dependencies change. This is critical when passing callbacks to memoized components or triggering effects.

Worked Example: Stabilizing a List Item

In our project, we have a ProductList component. We want to avoid re-rendering every ProductItem when the parent state updates, unless the specific item's data changes.

JSX
import React, { useCallback } from CE9178">'react';

const ProductList = ({ items }) => {
  // Without useCallback, this function is new every render.
  // Any child using this prop will re-render needlessly.
  const handleSelect = useCallback((id) => {
    console.log(CE9178">'Selecting:', id);
  }, []); // Dependency array: empty because it doesn't rely on state

  return (
    <ul>
      {items.map(item => (
        <MemoizedProductItem 
          key={item.id} 
          item={item} 
          onSelect={handleSelect} 
        />
      ))}
    </ul>
  );
};

Optimizing Expensive Calculations with useMemo

useMemo is for compute-heavy operations. If you're filtering a massive array or transforming data, you don't want to repeat that work on every render.

JSX
const expensiveData = useMemo(() => {
  return heavyCalculation(items);
}, [items]); // Only recalculate if CE9178">'items' changes

Managing Dependency Arrays

The primary pitfall with these hooks is the Dependency Array. If you omit a dependency, your hook will use stale data. If you include too many, the memoization becomes useless because the hook recalculates constantly.

The "Stale Closure" Trap

A common mistake is referencing a state variable inside a useCallback but forgetting to add it to the dependency array.

JSX
// BAD: CE9178">'count' is used but not in dependencies
const increment = useCallback(() => {
  setCount(count + 1);
}, []); // Stale closure! CE9178">'count' will always be the initial value.

The Fix: Use the functional update form of setState to avoid the dependency entirely:

JSX
const increment = useCallback(() => {
  setCount(prev => prev + 1);
}, []); // Perfectly stable

Hands-on Exercise

  1. Open your project's main dashboard component.
  2. Find a component that receives a callback prop.
  3. Use the Profiling with React DevTools techniques we learned earlier to confirm that the child component re-renders even when its data hasn't changed.
  4. Wrap the callback in useCallback and the data transformation logic in useMemo.
  5. Verify in the Profiler that the child no longer re-renders unnecessarily.

Common Pitfalls

  • Over-memoization: Memoization has a cost (memory and comparison overhead). Don't wrap every function or object in these hooks. Only use them when you've identified a performance bottleneck or are passing props to a React.memo component.
  • Ignoring the Dependency List: Use the eslint-plugin-react-hooks rule exhaustive-deps. It will catch 99% of your dependency errors.
  • Referential Equality confusion: Remember that useMemo returns a value, while useCallback returns a function. You cannot use useMemo to return a function unless it is a factory function returning a function.

Recap

useCallback and useMemo are your primary tools for stabilizing references in React. By ensuring that props remain referentially equal across renders, you allow React.memo to effectively bail out of rendering sub-trees, keeping your application snappy even as complexity grows. Remember: memoize only when you have a performance reason, and always keep your dependency arrays accurate.

Up next: We will look at State Colocation Strategies, where we move state closer to where it's actually used to further reduce the surface area of re-renders.

Similar Posts