Jan Hesters

useCallback vs. useMemo

What is the difference between useCallBack and useMemo? And why do useMemo and useCallback expect a function? If you’ve worked with React Hooks, you might have asked yourself these questions.

We will take a look at how they are distinct from another.

Note: This article assumes a basic understanding of Hooks. You should have read “Hooks at a Glance.”

Abstract

The React docs say that useCallback:

Returns a memoized callback.

And that useMemo:

Returns a memoized value.

In other words, useCallback gives you referential equality between renders for functions. And useMemo gives you referential equality between renders for values.

useCallback and useMemo both expect a function and an array of dependencies. The difference is that useCallback returns its function when the dependencies change while useMemo calls its function and returns the result.

Since JavaScript has first-class functions, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

Translation

Let’s understand and explain the abstract description above using simple examples. I’ll go over everything, but feel free to skip what you already know.

First-class functions

We said that in JavaScript functions are first-class. This means in JavaScript you can assign a function to a value.

// values
const number = 1;
const greeting = 'Hello';
 
// function declaration
function foo() {
  return 'bar';
}
 
// anonymous function expression - possible
// because functions are first-class
// and therefore usable as values
const otherFoo = function() {
  return 'bar';
};
 
// named function expression
const otherNamedFoo = function foo() {
  return 'bar';
};
 
// using arrow functions and implicit return
const anotherFoo = () => 'bar';
 
number; // 1
greeting; // 'Hello'
foo(); // 'bar'
otherFoo(); // 'bar'
otherNamedFoo(); // 'bar'
anotherFoo(); // 'bar'

Storing functions in variables enables you to use them as:

  • arguments to other functions,
  • return values from functions,
  • values to an object’s keys,
  • values in an array,
  • even as keys to the Map object.

Functions that return functions or take functions as input are called Higher-Order Functions.

const identity = x => x;
 
identity(1); // 1
 
// Used as Higher-Order Function
identity(foo);
 
// ƒ foo() {
//   return 'bar';
// }
identity(foo)(); // 'bar'

Referential equality

If you use JavaScript, you probably know that there are two equality comparison operators: == for abstract equality and === for strict equality. JavaScript’s equality can be weird, and there are many great articles already written about this topic. Go read those for an in-depth look as I will only cover the basic cases relevant for useCallback and useMemo.

Referential equality uses strict equality comparison which is true if the operands are of the same type and the contents match.

const greeting = 'hello';
const otherGreeting = 'hello';
 
function foo() {
  return 'bar';
}
const otherFoo = function() {
  return `bar`;
};
const anotherFoo = () => 'bar';
function sameFoo() {
  return 'bar';
}
const fooReference = foo;
 
'hello' === 'hello'; // true
greeting === otherGreeting; // true
 
foo === foo; // true
foo === otherFoo; // false
foo === anotherFoo; // false
foo === sameFoo; // false
foo === fooReference; // true

Notice how foo === sameFoo returns false (contrary to greeting === otherGreeting). This is because:

  1. two distinct objects are never equal for either strict or abstract comparisons,
  2. an expression comparing objects is only true if the operands reference the same object, and
  3. functions are objects in JavaScript.

foo and sameFoo have the same definition but reference two different objects.

Memoization

Memoization is a way to speed up performance by cutting down on computations.

When you first calculate a result, you save it. If you need the result for the same arguments again, you use the saved one instead of recalculating.

Here is a simple memoize function in JavaScript.

function memoize(fn) {
  const cache = new Map();
 
  return function(...args) {
    const key = args.toString();
 
    if (cache.has(key)) {
      return cache.get(key);
    }
 
    const result = fn.apply(this, args);
    cache.set(key, result);
 
    return result;
  };
}
 
function add(a, b) {
  return a + b;
}
 
const memoizedAdd = memoize(add);
 
console.log(memoizedAdd(2, 3));  // Calculates and caches result
console.log(memoizedAdd(2, 3));  // Returns cached result

In the example above, you save the result of adding 2 and 3. For each call after the first, the cached result is used.

API

The APIs of useCallback and useMemo look similar. They both take in a function and an array of dependencies.

useCallback(fn, deps);  
useMemo(fn, deps);

So what is the difference? useCallback returns its function uncalled so you can call it later, while useMemo calls its function and returns the result.

function foo() {
  return 'bar';
}
 
const memoizedCallback = useCallback(foo, []);
const memoizedResult = useMemo(foo, []);
 
memoizedCallback;
// ƒ foo() {
//   return 'bar';
// }
memoizedResult; // 'bar'
memoizedCallback(); // 'bar'
memoizedResult(); // 🔴 TypeError

Side note: Now - knowing about first class functions - you should understand why useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).


In the real world, the examples above are meaningless. I just gave it to help you understand the API. If you can pass a function to useCallback or useMemo directly and you can use that function with empty dependencies, you could define the function outside of the component (and do without useCallback or useMemo). Therefore, in the real world, you will see something like this.

function MyComponent({ foo, initial }) {
  // ...
  const memoizedCallback = useCallback(() => {
    // do something with foo and bar
    someFunc(foo, bar);
  }, [foo, bar]);
  const memoizedResult = useMemo(() => someOtherFunc(foo, bar), [
    foo,
    bar,
  ]);
  // ...
}

useCallback usually takes in an inline callback that calls the function that uses the dependencies. And useMemo takes in a “create function” that calls some function and returns its results.


Another side note: The following might be trivial for you, but I had to think about it a long time to get it. The following code is wrong. You can’t use useCallback to memoize values. Meaning useCallback(fn(), deps) isn’t a thing.

function sum(a, b) {
  console.log('sum() ran');
  return a + b;
}
 
function App() {
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const [name, setName] = useState('Jim');
 
  const result = useCallback(sum(val1, val2), [val1, val2]);
 
  return (
    <div className="App">
      <input
        value={val1}
        onChange={({ target }) =>
          setVal1(parseInt(target.value || 0, 10))
        }
      />
      <input
        value={val2}
        onChange={({ target }) =>
          setVal2(parseInt(target.value || 0, 10))
        }
      />
      <input
        placeholder="Name"
        value={name}
        onChange={({ target }) => setName(target.value)}
      />
      <p>{result}</p>
    </div>
  );
}

Whenever you change name sum recalculates. useCallback and useMemo are so useful because they allow lazy evaluation. result is evaluated eagerly on every render. Try it out and take a look at your console.


So why do we need two Hooks dedicated to memoizing values?

Usage

You want to use useCallback or useMemo whenever you depend on referential equality between renders. I find myself mostly using it for useEffect, React.memo and useMemo to replace shouldComponentUpdate from React.PureComponent because the dependencies of these hooks get checked for referential equality.

Let’s say you have a component that given a userId displays the user’s data.

import React, { useEffect, useState } from 'react';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
 
function User({ userId }) {
  const [user, setUser] = useState({ name: '', email: '' });
 
  const fetchUser = async () => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    const newUser = await res.json();
    setUser(newUser);
  };
 
  useEffect(() => {
    fetchUser();
  }, []);
  
  return (
    <ListItem dense divider>
      <ListItemText primary={user.name} secondary={user.email} />
    </ListItem>
  );
}
 
export default User;

Can you spot the mistake(s)?

If you have eslint-plugin-react-hooks installed, it will yell at you for omitting fetchUser from useEffect’s dependencies. But mindlessly adding it will actually create an infinite loop!

In React functions defined in function components get re-created on every render because of closure, which results in referential inequality.

const fetchUser = async () => {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
  const newUser = await res.json();
  setUser(newUser); // 🔴 setState triggers re-render
};
 
useEffect(() => {
  fetchUser();
}, [fetchUser]); // fetchUser is a new function on every render

There are two ways to solve this issue. Admittedly the most elegant would be to move fetchUser inside useEffect and add userId as a dependency.

// 1. Way to solve the infinite loop
useEffect(() => {
  const fetchUser = async () => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    const newUser = await res.json();
    setUser(newUser); // Triggers re-render, but ...
  };
 
  fetchUser();
}, [userId]); // ✅ ... userId stays the same.

But, this is an article about useCallback and useMemo, so let’s solve the problem using the former. (And, maybe you would like to use fetchUserfunction in multiple places.)

You can define fetchUser with useCallback so that the function stays the same, unless userId changes.

// 2. Way to solve the infinite loop
const fetchUser = useCallback(async () => {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
  const newUser = await res.json();
  setUser(newUser);
}, [userId]);
 
useEffect(() => {
  fetchUser();
}, [fetchUser]); // ✅ fetchUser stays the same between renders

And lastly, let’s say you filter all users and display them in a list.

// Some FP magic 🧙🏼‍♂️
const filter = (f, arr) => arr.filter(f);
const prop = key => obj => obj[key];
const getName = prop('name');
const strIncludes = query => str => str.includes(query);
const toLower = str => str.toLowerCase();
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const nameIncludes = query =>
  pipe(
    getName,
    toLower,
    strIncludes(toLower(query))
  );
 
function UserList({ query, users }) {
  // 🔴 Recalculated on every render
  const filteredUsers = filter(nameIncludes(query), users);
  // ...
}

This is inefficient. We can use useMemo to only recalculate the filtered users, when the query changes.

function UserList({ query, users }) {
  // ✅ Recalculated when query or users change
  const filteredUsers = useMemo(
    () => filter(nameIncludes(query), users),
    [query, users]
  );
  // ...
}

Learn senior fullstack secrets

Subscribe to my newsletter for weekly updates on new videos, articles, and courses. You'll also get exclusive bonus content and discounts.