[React Basics] useCallback and useMemo: Mastering React App Performance Optimization

If you've ever worked with React, you've probably heard about the never-ending battle against the enemy called unnecessary re-renders. This is where useCallback and useMemo appear as saviors, helping your app run smoother, faster, and more efficiently.

useCallback and useMemo: Mastering React App Performance Optimization

But what exactly are they? How do they work? And most importantly, when should (and shouldn't) you use them? Let's unveil the mystery behind this "power duo"!

The Root of the Problem: Why Are Re-renders the "Bad Guy"?

To understand the power of useCallback and useMemo, we first need to understand the "enemy" we're facing. In React, a component re-renders whenever its state or props change.

The problem lies in how JavaScript handles equality for objects and functions.

// Every comparison, even if they look identical, results in two different objects in memory!
console.log({} === {}) // false
console.log([] === []) // false
console.log((() => {}) === (() => {})) // false

What does this mean in React? Every time a parent component re-renders, all functions and objects defined inside it are recreated. Even if the function's code hasn't changed, its reference in memory is different.

When you pass these new functions or objects down to child components as props, React thinks "Oh, the props have changed!" and causes the child to re-render unnecessarily. This is where your app's performance can start to suffer.

useMemo: When You Want to "Remember" an Expensive Value 🧠

useMemo is a hook that lets you memoize the result of an expensive computation. It will only recalculate that value when one of its dependencies changes.

Syntax:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
  • Computation function: () => computeExpensiveValue(a, b) is where you perform heavy calculations (filtering large arrays, complex math, etc.).
  • Dependency array: [a, b] is the list of values that useMemo watches. If any value in this array changes, the computation runs again. Otherwise, useMemo returns the memoized value from the previous render.

When should you use useMemo?

  1. Optimizing heavy computations: When you have a function that processes large data or is time-consuming, wrap it in useMemo to avoid recalculating on every render.
  2. Maintaining reference stability for objects/arrays: When you need to pass an object or array to a child component, use useMemo to ensure its reference doesn't change if its contents haven't changed.

Real-world example:

Imagine you have a product list and a function to filter expensive products.

// Without useMemo: The filter runs every time the component re-renders
const expensiveProducts = products.filter((product) => product.price > 1000)

// With useMemo: The filter only runs when 'products' changes
const expensiveProducts = useMemo(() => {
  console.log('Filtering expensive products...') // You'll see this log less often
  return products.filter((product) => product.price > 1000)
}, [products])

useCallback: When You Want to "Remember" a Function 📜

Very similar to useMemo, but instead of memoizing a function's return value, useCallback memoizes the function itself.

Syntax:

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])

Simply put, useCallback returns an "immutable" version of your function. This version only changes if one of the dependencies in [a, b] changes.

Fun fact: useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). useCallback is just a convenient shorthand for this specific case.

When should you use useCallback?

  1. Passing callbacks to optimized child components: This is the most common and important use case. When you have a child component wrapped in React.memo and you need to pass an event handler (like onClick, onSubmit), use useCallback. This prevents creating a new function on every render, allowing React.memo to work effectively.
  2. As a dependency for other hooks: When a function is used inside useEffect, useLayoutEffect, or even useMemo, wrapping it in useCallback helps avoid infinite loops or unnecessary effect reruns.

Real-world example:

const ParentComponent = () => {
  const [count, setCount] = useState(0)

  // Without useCallback, this function is recreated on every render
  // const handleIncrement = () => console.log("Increment!");

  // With useCallback, handleIncrement is only created once
  // and its reference doesn't change between renders.
  const handleIncrement = useCallback(() => {
    console.log('Increment!')
  }, []) // Empty dependency array since the function doesn't depend on any value

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Re-render Parent</button>
      {/* ChildComponent won't re-render unnecessarily anymore */}
      <ChildComponent onIncrement={handleIncrement} />
    </div>
  )
}

// React.memo does a shallow comparison of props; if props don't change, it won't re-render
const ChildComponent = React.memo(({ onIncrement }) => {
  console.log('Child re-rendered!')
  return <button onClick={onIncrement}>Increment Button</button>
})

Head-to-Head: useCallback vs. useMemo

Syntax of useCallback and useMemo

Quick comparison table of the most important criteria:

CriteriauseMemouseCallback
PurposeMemoize a value (function result)Memoize a function
ReturnsThe memoized value (string, number, object, array)The memoized function
Similar to"Remember the answer to this problem""Remember the solution method to this problem"
Use caseHeavy computations, stable object/array propsPassing callbacks to child, hook dependencies

Pitfalls to Avoid: When NOT to Use Them ❌

Although powerful, overusing useCallback and useMemo can backfire. Remember: Optimization also has a cost.

These hooks themselves require memory to store values/functions and time to compare dependencies.

Golden rules:

  1. Don't optimize prematurely: Don't blindly wrap everything in useCallback/useMemo.
  2. Measure before acting: Use tools like React DevTools Profiler to pinpoint which components are re-rendering unnecessarily and causing performance bottlenecks.
  3. Top priority: The clearest and most beneficial use case is when passing props (functions or objects) to child components wrapped in React.memo.

Conclusion: The Perfect Trio—React.memo, useCallback, useMemo

Think of them as a team. React.memo is the gatekeeper, blocking unnecessary re-renders. But this guard is only effective when useCallback and useMemo provide it with "stable passes" (props) that don't change reference on every render.

  • Use useMemo to memoize values.
  • Use useCallback to memoize functions.
  • Both serve to maintain referential equality, preventing unnecessary re-renders.

Mastering this duo not only makes your app faster but also shows you have a deep understanding of React performance. Happy coding! 🚀

Related Posts

[React Basics] Rendering Lists in React: Best Practices and Performance Optimization

Learn techniques for rendering lists in React, from basic .map() usage to advanced methods for handling dynamic data and optimizing performance. This guide is for all levels.

Implementing React Multi-language: Best Practices & Performance Optimization

Learn the best methods to integrate multi-language support into React applications, ensuring high performance and easy maintenance. Elevate your application!

[React Basics] Mastering the useRef Hook in React with Practical Examples

What is the useRef hook? Learn how to use useRef in React to interact with the DOM and store values without causing re-renders. Includes clear and detailed code examples.

[React Basics] Form Handling in React: Master State, Controlled Components, and Validation

Form handling in React is easier than ever. This article explains controlled components and how to build a complete form from start to finish.