React useMemo
Lorenzo Zarantonello
Posted on November 10, 2023
useMemo is a React Hook that allows you to memoize the result of a calculation.
Memoization caches the result of a calculation to avoid recalculating it every time it is needed.
Memoization can improve performance by reducing the number of times expensive calculations need to be performed.
React.dev shows an example of useMemo.
import { useMemo } from 'react';
const cachedValue = useMemo(calculateValue, dependencies)
where
-
calculateValue
is the function calculating the value to cache. The function should be a pure function and should take no arguments. It can return any type. -
dependencies
is an array of all the values used insidecalculateValue
.
useMemo In React Lifecycle
First, React calls useMemo during the initial render, on component mount. The value returned from the calculatedValue
function is stored in a variable (cachedValue in the example above).
Then, for all the next renders, React has a choice:
- If the dependencies have not changed, use the same value.
- If any dependency changes, call calculateValue and cache the newly returned value so it can be reused later.
If the component re-renders for other reasons, useMemo ignores the calculateValue
function and uses the cached value.
Examples
1. Separate Components & Push State Down
Sometimes, you can avoid memoization altogether by pushing the state down, as suggested by Josh Comeau.
The reasoning goes like this.
If you have two unrelated components, they can manage their own state independently when there is no reason to lift the state up.
In so doing, the two components are independent. In other words, the rendering and re-rendering of ComponentOne doesn't affect ComponentTwo, and vice-versa.
If the heavy computation happens in one component, the re-rendering of the other component doesn't trigger heavy computations again.
In this example on StackBlitz, I am using the Clock component from Josh Comeau's article, and I am fetching data from jsonplaceholder in a second component called Users.
Since Clock and Users are independent of each other, React re-renders the Clock component every second but not the Users component, which contains an expensive HTTP request.
// App.tsx
...
export const App: FC<{ name: string }> = ({ name }) => {
return (
<div>
<h1>Hello {name}!</h1>
<Clock />
<Users />
</div>
);
};
This solution is also recommended on react.dev: "Prefer local state and don’t lift state up any further than necessary."
However, in some cases, it is not possible to push the state down. But maybe you can lift the content up, as suggested by Dan Abramov in Before you memo().
2. Lift Content Up & Use Children Prop
Here is the example of Dan Abramov, where the ExpensiveTree components shouldn't be re-rendered if possible.
export default function App() {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
A possible solution is to "split the App component in two. The parts that depend on the color, together with the color state variable itself, have moved into [a new component called] ColorPicker"
// ColorPicker
function ColorPicker({ children }) {
let [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}
"The parts that don’t care about the color stayed in the App component and are passed to ColorPicker as JSX content, also known as the children prop."
So the restructured App component becomes
// App.tsx
...
export default function App() {
return (
<ColorPicker>
<p>Hello, world!</p>
<ExpensiveTree />
</ColorPicker>
);
}
"When the color changes, ColorPicker re-renders. But it still has the same children prop it got from the App last time, so React doesn’t visit that subtree.
And as a result, doesn’t re-render."
This solution is also recommended by react.dev: "When a component visually wraps other components, let it accept JSX as children. This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render."
If these solutions don't work, we have to resort to memoization.
3. Memoize Components
Let's go back to the first example where we pushed the state down.
If the Clock component logic cannot be pushed down, it remains in the App component. React re-renders the App component (and all children) every time a change occurs (every second). You can find this example on this StackBlitz.
// App.tsx
...
// Use useMemo around Users
export const App: FC<{ name: string }> = ({ name }) => {
const time = useTime();
return (
<div>
<h1>Hello {name}!</h1>
<p>{format(time, 'hh:mm:ss a')}</p>
<Users />
</div>
);
};
To keep Users from re-rendering and triggering a fetch request every second we can wrap Users around useMemo before exporting it.
// Users.tsx
...
const Users: FC = () => { ... }
export default React.memo(Users);
This is slightly different from the useMemo
syntax we saw above.
const cachedValue = useMemo(calculateValue, dependencies)
That is because we are using memo
and not useMemo
.
The main difference between memo and useMemo is that memo is a higher-order component (HOC) and useMemo is a React Hook.
memo is used to memoize a React component, which means that it caches the output of the component and only re-renders it if its props have changed. This can be useful when a component's rendering is expensive, and you want to avoid unnecessary re-renders.
useMemo is used to memoize the result of a function inside a component. This can be useful when you need to perform an expensive calculation, and you want to avoid recalculating it every time the component re-renders.
4. Memoize Components And Change Props
There is no array of dependencies in memo
. So what if we need to trigger the expensive operation again?
If we are memoizing a component, the component will only be re-rendered if its props change.
So here is a memoized component with changing props.
First, we added three buttons so you can choose what data you want to fetch:
Once you click a button, the selected item will be saved in the item
variable and passed to the Users component
// App.tsx
...
export const App: FC<{ name: string }> = ({ name }) => {
const [item, setItem] = useState<Items>('albums');
const time = useTime();
return (
<div>
<h1>Hello {name}!</h1>
<p>{format(time, 'hh:mm:ss a')}</p>
<button onClick={() => setItem('users')}>Users</button>
<button onClick={() => setItem('posts')}>Posts</button>
<button onClick={() => setItem('albums')}>Albums</button>
<Users item={item} />
</div>
);
};
Second, we changed the code in Users to use the prop passed in from App.tsx.
The new prop is called item
and is of type Items (you can see all types I used on StackBlitz).
The string assigned to item
is then used to fetch the correct data from jsonplaceholder. Fetched data is saved in items
and used in the template to render a list of items in the Users component (at this point we should rename the component because it can render other things).
// Users.tsx
const Users: FC = ({ item }: { item: Items }) => {
const [items, setItems] = useState<User[] | Album[] | Post[]>(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/${item}`)
.then((response) => response.json())
.then((data) => setItems(data));
}, [item]);
console.log(items);
return (
<div>
{items &&
items.map((item: User | Album | Post) => (
<p key={item.id}>
{item.name} {item.title}
</p>
))}
</div>
);
};
export default React.memo(Users);
Note that when you look at the console, you will see two logs every time you select an item. This is because fetching data is an asynchronous operation.
The first log will print the data that is already in items
(the old data) and the second log will print the newly fetched data.
This works fine as long as item
is not a collection (e.g. not an array or object).
The problem with arrays is that "every time React re-renders, we're producing a brand new array. They're equivalent in terms of value, but not in terms of reference."
In other words, they are using two different spaces in your computer's memory.
So, if item
is an array (let's call it itemArray), we can memoize it as follows:
const itemArray = React.useMemo(() => {
return [
{ id: 1, value: 'users' },
{ id: 2, value: 'posts' },
{ id: 3, value: 'albums' },
];
}, []);
This closes the circle by going back to the initial useMemo
syntax we started with.
However, I encourage you to read a more comprehensive explanation about Preserved References
Summary
Memoization caches the result of a calculation to avoid recalculating it every time it is needed.
Use the HOC memo to memoize components and useMemo to memoize the result of a function.
Before using memoization, try to push the state down or lift the content up.
In alternative use memoization.
As a rule of thumb,
- Only memoize values that are expensive to calculate.
- Don't memoize values that are frequently updated.
Nice Reads
Posted on November 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 18, 2024