What is the difference between useCallBack
and useMemo
? And why do useMemo
and useCallback
expect a function? If you're a React developer and 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.”.
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).
Want to see this tutorial in action? Check it out on YouTube!
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.
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';
}
// named function expression - possible
// because functions are first-class
// and therefore usable as values
const otherFoo = function() {
return 'bar';
};
// using arrow functions and implicit return
const anotherFoo = () => 'bar';
number; // 1
greeting; // 'Hello'
foo(); // 'bar'
otherFoo(); // 'bar'
anotherFoo(); // 'bar'
Storing functions in variables enables you to use them as (the cursive points are important for React Hooks):
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'
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
foo
and sameFoo
have the same definition but reference two different objects.
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.
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 namesum
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?
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 fetchUser
function 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]
);
// ...
}
useCallback
you can define a function that has referential equality between renders.useMemo
to calculate a value that has referential equality between renders.Both only change their return value when their dependencies change.
If you liked this article, you should subscribe to my YouTube channel 🤗