Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Optimizing Performance with `useMemo` and `useCallback`

Recommended resources

Hello! Welcome to the fifth lesson in our "React Fundamentals Refresher" module.

In our last lesson, we focused on the crucial role of the useEffect cleanup function in preventing memory leaks from timers and event listeners. We established a core principle for building stable apps: always clean up the resources you create.

Today, we shift our focus from preventing bugs to actively improving performance. As you're keen to understand the "under-the-hood" mechanics of React Native, this lesson dives into two of React's most important optimization tools: useMemo and useCallback. We'll explore how they help you prevent unnecessary, expensive work, leading to a smoother and more responsive user interface. By the end, you will be able to apply these hooks to prevent both unnecessary re-computations and re-renders.

The Cost of Re-Renders

First, let's briefly recap what causes a component to re-render. As you know, React keeps the UI in sync with your app's state. When something changes, it re-renders to create a new "snapshot" of the UI.

A component re-renders if:

  1. Its own state changes (e.g., via useState).
  2. Its props change.
  3. A context it consumes changes value.
  4. Its parent component re-renders.

This last point is the source of many performance issues. By default, when a parent component re-renders, it re-renders its entire child subtree, even if the children's props haven't changed. While React is very fast, this can become a problem in two main scenarios:

  • Expensive Calculations: A component performs a computationally intensive task on every render (e.g., sorting a large array).
  • Unnecessary Child Renders: A child component re-renders just because its parent did, even though nothing visually needs to change for that child.

Memoization hooks are our primary tools for addressing these two problems.


useMemo: Avoiding Expensive Calculations

Let's start with useMemo. Its purpose is to "remember" or memoize the result of an expensive calculation and only re-compute it when its inputs have actually changed.

Imagine a component that displays a list of prime numbers. Calculating these primes can be slow. If an unrelated piece of state in the component (like a theme toggle) changes, the component re-renders, and we would wastefully re-calculate the same list of primes.

useMemo solves this by caching the result.

const expensiveResult = useMemo(() => {
  // This function only runs when a or b changes.
  return computeExpensiveValue(a, b);
}, [a, b]); // The dependency array

The following article from Josh W. Comeau provides an excellent explanation of this concept using a prime number generator and a clock.

Understanding useMemo and useCallback

Let's read from the article 'Understanding useMemo and useCallback' to grasp the fundamental problem these hooks solve. We'll focus on the first use case: optimizing heavy computations.

Read from the beginning up to the end of the section 'Use case 1: Heavy computations'. Focus on: The 'snapshot' mental model of React rendering. How an unrelated state change (the clock) can trigger an expensive, unnecessary re-calculation. The syntax of useMemo and how its dependency array acts as a 'cache invalidation strategy'.


Preventing Re-Renders with React.memo and useCallback

The second, and more common, use case for memoization is preventing components from re-rendering unnecessarily. React provides a helper for this called React.memo. A component wrapped in React.memo will only re-render if its props have changed.

const MyComponent = ({ someProp }) => { /* ... */ };
export default React.memo(MyComponent);

However, there's a catch. This brings us to a fundamental JavaScript concept you must understand to optimize React apps: referential equality.

The Problem: Referential Equality

In JavaScript, primitive types (strings, numbers, booleans) are compared by their value.
5 === 5 is true.

However, objects, arrays, and functions are compared by reference. They are only considered equal if they point to the exact same object in memory.

const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false! They look the same but are two different objects.

const obj3 = obj1;
console.log(obj1 === obj3); // true! They point to the same object.

The video below gives a fantastic, non-React explanation of this concept. Understanding this is essential.

REACT MEMO vs USECALLBACK vs USEMEMO

Before we dive into useCallback, we need to understand a core JavaScript concept that causes many performance issues in React: referential equality. This video provides a crystal-clear explanation of why { a: 1 } !== { a: 1 }.

Watch from 04:13 to 07:21. Pay close attention to the distinction between comparing primitive values (like numbers) and object references. This is the entire foundation for why useCallback is necessary.

Because functions are objects in JavaScript, a new function defined inside a component is created on every single render. If you pass this function as a prop to a React.memo-wrapped child, React.memo will see a "new" prop on every render and re-render the child, defeating the purpose of memoization.

The next section of the Josh W. Comeau article perfectly illustrates this problem.

Understanding useMemo and useCallback

Now let's see how referential equality affects React components. This next section of the article explains why a React.memo-wrapped component can still re-render unnecessarily.

Read the sections 'Use case 2: Preserved references' and 'The useCallback hook'. The key insight here is understanding that on every parent render, a new array, object, or function is created, which React sees as a new prop, breaking memoization. Then see how useCallback is introduced as the solution for functions.

The Solution: useCallback

As you just read, useCallback is the solution. It does for functions what useMemo does for values: it memoizes the function itself. It returns the exact same function reference between renders, as long as its dependencies don't change.

This ensures that from the child component's perspective, the prop has not changed, and React.memo can successfully skip the re-render.


Putting It All Together: A React Native Example

We've covered the theory. Now, let's see it all come together in a practical React Native scenario. This video from notJust.dev demonstrates a common performance bottleneck with a FlatList and how to solve it using the hooks we've discussed.

React Native Performance Optimisation (useMemo, useCallback, memo)

This video walks through a complete optimization workflow, starting with a performance problem and applying React.memo, useCallback, and useMemo to fix it.

Watch from 01:04 to 14:38. This is a comprehensive walkthrough. Follow these steps: The Problem (01:04 - 03:35): Notice how a simple state change in the parent causes every list item to re-render. First Fix with React.memo (03:26 - 04:55): See how React.memo initially solves the problem. Breaking Memoization (04:43 - 07:48): Observe how passing a new function as a prop on every render breaks the React.memo optimization due to referential equality. Fixing with useCallback (07:36 - 10:01): Understand how useCallback preserves the function's reference, making React.memo work again. Introducing useMemo (09:49 - 14:38): See how useMemo is used to prevent an expensive calculation from running on every render.


useMemo vs. useCallback: The Summary

To put it as simply as possible:

  • useMemo invokes its function and memoizes the return value.
  • useCallback memoizes the function definition itself.
useMemo vs useCallback Comparison
A visual comparison of the `useMemo` and `useCallback` hooks. `useMemo` is used to cache a computed *value*, while `useCallback` is used to cache a *function* itself to maintain a stable reference.

As the notJust.dev video states: "useCallback returns its function when dependencies change, while useMemo calls its function and returns the result when dependencies change."


Best Practices & When NOT to Memoize

Given your role as CTO, making pragmatic technical decisions is key. Memoization is not free; it uses memory to store the memoized values and adds a small overhead to compare dependencies on every render. Over-optimizing can make code more complex and can sometimes even harm performance.

The golden rule is: profile first, then optimize. Use tools like the React DevTools Profiler (which we'll cover later) to find actual bottlenecks before reaching for these hooks.

This final reading provides excellent, pragmatic guidelines on when to use memoization and common mistakes to avoid.

How to Optimize React Native Re-Renders with useMemo and ...

Knowing when not to optimize is as important as knowing when to optimize. This article provides excellent best practices and highlights common pitfalls.

Read the sections 'Dependency Arrays Best Practices', 'Common Mistakes to Avoid', and 'When NOT to Use These Hooks'. Focus on these key rules: Rule of thumb: Profile first, optimize second. Don't prematurely optimize. Dependency arrays: Include everything the hook reads. Use functional state updates to reduce dependencies where possible. Mistakes: Avoid over-memoizing simple calculations, forgetting React.memo when using useCallback for props, and passing inline objects/arrays as props to memoized children. Cost: Remember that memoization has a memory and comparison cost. Only use it when the benefit outweighs the cost.


Key Takeaways

  • Unnecessary re-renders and re-computations can harm your app's performance.
  • useMemo caches the result of an expensive calculation, preventing it from re-running unnecessarily.
  • useCallback caches a function definition, preserving its reference to prevent child components from re-rendering.
  • The combination of React.memo on a child component and useCallback for any function props passed to it is the standard pattern for preventing unnecessary re-renders.
  • Referential equality is the core concept explaining why these hooks are needed for objects, arrays, and functions.
  • Don't over-optimize. Profile your application to find real bottlenecks before applying memoization.

You've now covered all the core React hooks for managing state, side effects, and performance. This foundational knowledge is essential for building high-quality, scalable applications.

In our next lesson, we'll conclude this module by learning how to create custom hooks. This powerful pattern will allow you to extract and reuse stateful logic across your application, often combining the very hooks we've just reviewed (useState, useEffect, useCallback, etc.).