Memoization has to do with caching. Here's a super simple implementation of memoization:
if (!cache.hasOwnProperty(input)) {
The basic idea is: hang on to the input and their associated output and return that output again if called with the same input.
The point is to avoid re-calculating a value for which you already have the result cached. In our case, we're avoiding "input + 2" 🙃
addTwo(3) // 5, but this time we got it from the cache 🤓
// (we didn't have to recalculate it)
// 🤓 I'll show up when we've memoized something
Maybe not entirely worthwhile for this calculation, but it could/would be for an expensive one.
Another interesting aspect to memoization is the fact that the cached value you get back is the same one you got last time. So:
// let's imagine we have a function that returns an array of matching
// assuming getPostsNoMemo is not memoized
const posts1 = getPostsNoMemo('search term')
const posts2 = getPostsNoMemo('search term')
posts1 === posts2 // false (unique arrays)
// assuming getPostsMemo is memoized
const posts1 = getPostsMemo('search term')
const posts2 = getPostsMemo('search term')
posts1 === posts2 // true (identical array) 🤓
This has interesting implications for React we'll talk about in a second...
From there you need to talk about cache invalidation. If it's a pure function you're memoizing, then you could keep the cache around forever... except you might run into "out of memory" issues depending on how large the cache gets. Cache invalidation is tricky and I'm not going to get into that today.
React's memoization
React has three APIs for memoization: memo
, useMemo
, and useCallback
. The caching strategy React has adopted has a size of 1. That is, they only keep around the most recent value of the input and result. There are various reasons for this decision, but it satisfies the primary use case for memoizing in a React context.
So for React's memoization it's more like this:
let prevInput, prevResult
if (input !== prevInput) {
With that:
addTwo(3) // 5 is computed
addTwo(3) // 5 is returned from the cache 🤓
addTwo(2) // 4 is computed
addTwo(3) // 5 is computed
To be clear, in React's case it's not a !==
comparing the prevInput. It checks equality of each prop and each dependency individually. Let's check each one:
// React.memo's `prevInput` is props and `prevResult` is react elements (JSX)
const MemoComp = React.memo(Comp)
// then, when you render it:
<MemoComp prop1="a" prop2="b" /> // renders new elements
// rerender it with the same props:
<MemoComp prop1="a" prop2="b" /> // renders previous elements 🤓
// rerender it again but with different props:
<MemoComp prop1="a" prop2="c" /> // renders new elements
// rerender it again with the same props as at first:
<MemoComp prop1="a" prop2="b" /> // renders new elements
// React.useMemo's `prevInput` is the dependency array
// and `prevResult` is whatever your function returns
const posts = React.useMemo(() => getPosts(searchTerm), [searchTerm])
// initial render with searchTerm = 'puppies':
// - posts is a new array of posts
// rerender with searchTerm = 'puppies':
// - getPosts is *not* called
// - posts is the same as last time 🤓
// rerender with searchTerm = 'cats':
// - posts is a new array of posts
// rerender render with searchTerm = 'puppies' (again):
// - posts is a new array of posts
// React.useCallback's `prevInput` is the dependency array
// and `prevResult` is the function
const launch = React.useCallback(
() => launchCandy({type, distance}),
// initial render with type = 'twix' and distance = '15m':
// - launch is equal to the callback passed to useCallback this render
// rerender with type = 'twix' and distance = '15m':
// - launch is equal to the callback passed to useCallback last render 🤓
// rerender with same type = 'twix' and distance '20m':
// - launch is equal to the callback passed to useCallback this render
// rerender with type = 'twix' and distance = '15m':
// - launch is equal to the callback passed to useCallback this render
The value of memoization in React
There are two reasons you might want to memoize something:
- Improve performance by avoiding expensive computations (like re-rendering expensive components or calling expensive functions) 2. Value stability
I think we've covered the first point, but I want to make something clear about the value stability benefit. In a React context, this value stability is critical for memoization of other values as well as side-effects. Let's look at a simple example:
const [body, setBody] = React.useState()
const [status, setStatus] = React.useState('idle')
headers: {'content-type': 'application/json'},
const makeFetchRequest = () => (body ? fetch('/post', fetchConfig) : null)
const promise = makeFetchRequest()
// if no promise was returned, then we didn't make a request
() => setStatus('fulfilled'),
() => setStatus('rejected'),
function handleSubmit(event) {
<form onSubmit={handleSubmit}>
{/* form inputs and other neat stuff... */}
Please note: this might not be the way you'd write form submission code, it's > not how I'd do it either... Really, I'd just use react-query personally, but > bear with me for the purpose of the example...
Take a guess at what's going to happen. If you guessed "runaway side-effect loop" you're right! The reason is because React.useEffect
will trigger a call to the given effect callback whenever individual elements of the dependency array changes. Our only dependency is makeFetchRequest
and makeFetchRequest
is created within the component and that means it's new every render.
So this is where the value stability of memoization plays an important role in React. So let's memoize makeFetchRequest
with useCallback
:
const makeFetchRequest = React.useCallback(
() => (body ? fetch('/post', fetchConfig) : null),
useCallback
will only return a new function when the dependencies change. And because of that, makeFetchRequest
has a stable value between renders. Unfortunately, fetchConfig
is also created within the component and that means it's new every render as well. So let's memoize that for value stability:
const fetchConfig = React.useMemo(() => {
headers: {'content-type': 'application/json'},
Great! So now the fetchConfig
and makeFetchRequest
will both be stable and will only change when the body
changes which is what we want. Here's the final version of this code:
const [body, setBody] = React.useState()
const [status, setStatus] = React.useState('idle')
const fetchConfig = React.useMemo(() => {
headers: {'content-type': 'application/json'},
const makeFetchRequest = React.useCallback(
() => (body ? fetch('/post', fetchConfig) : null),
const promise = makeFetchRequest()
// if no promise was returned, then we didn't make a request
() => setStatus('fulfilled'),
() => setStatus('rejected'),
function handleSubmit(event) {
<form onSubmit={handleSubmit}>
{/* form inputs and other neat stuff... */}
The value stability provided by useCallback
for makeFetchRequest
helps us make sure that we can control when our side-effect runs. And the value stability provided by useMemo
for fetchConfig
helps us preserve memoization characteristics for makeFetchRequest
so that can work.
I don't find myself having to do this stuff a whole lot, but when I need to it's nice to know how. Like I said, I'd just use react-query for this kind of thing, but if I didn't want to, this is how I would actually write this (short of abstracting it away myself):
const [body, setBody] = React.useState()
const [status, setStatus] = React.useState('idle')
// no need to do anything if we don't have a body to send
headers: {'content-type': 'application/json'},
fetch('/post', fetchConfig).then(
() => setStatus('fulfilled'),
() => setStatus('rejected'),
function handleSubmit(event) {
<form onSubmit={handleSubmit}>
{/* form inputs and other neat stuff... */}
And now I don't need to worry about memoizing anything anyway! Like I said, I don't need to memoize things super often, but when I do, it's nice to know why it's needed and what I'm really doing. Hopefully I helped you understand that too. Good luck!