Ever wondered what memoization really means when working in React? Have you used useMemo
or useCallback
hooks or even React.memo
countless times without actually understanding their particularities?
You’re in the right place! This blog post will teach you everything you need to know to understand memoization; from the best time to memoize to a working demo, we have you covered. By the end of this post, memoization in React will no longer be a question mark to you.
What is Memoization?
Memoization is an optimization technique used to speed up computer programs in exchange for higher use of memory space.
The speed-up is a result of avoiding repetitive computation of results when the same input parameters are provided, and the cached result is used instead. At the same time, the higher usage of memory space results from the cached result taking that extra space.
If you need to address any of the following issues, then you should probably be memoizing:
-
An expensive function with high computation can have its result cached for better performance.
-
A large component that will render the same results given the same input, also known as a
Pure Component
. In that case, it can be memoized to skip unnecessary expensive renders when the parent re-renders, but the props of thatPure Component
have not changed. -
Maintaining referential integrity for objects (including functions) when being passed as props to pure components or as dependencies to a dependency array.
How Does Memoization Work?
For some memoization techniques, React performs a shallow equality on its components’ props to decide if the component should be re-rendered. For example, React.memo
& React.PureComponent
.
If you want to do more than a shallow comparison, React.memo
can take a custom equality function as a second argument, while React.PureComponent
shouldComponentUpdate
lifecycle can be overridden.
On the other hand, useMemo()
& useCallback()
rely on their dependency array to decide whether the cached value should be invalidated or not.
If the dependency array is empty that indicates that cache invalidation is expected not to happen.
Drawbacks of Premature Memoization
Before you get too far, I want to warn you against premature optimization.
You should only memoize sections of your application that you have identified as performance bottlenecks. Adding memoization to code that doesn’t need it can make your program worse instead of better.
For example:
-
CPU/memory cost of memoization, as mentioned before memoization works by caching results which consume more memory space. In addition, memoization requires some sort of props/dependencies comparison to deduct whether the cached result should be returned or not which also requires extra processing.
-
Memoization in React adds extra complexity to your existing code plus more dependencies to manage. All of the additional complexity is overhead that you need to count for.
-
You should not assume that those cached values as a result of memoization will not get recomputed unexpectedly. React may decide to invalidate the cached when needed. so to avoid introducing bugs, only use memoization for performance optimization.
As per React documentation on useMemo:
You may only rely on memoization in React as performance optimization and not as a semantic guarantee.
You will find a similar statement in the documentation on React.memo:
This method only exists as a performance optimization. Do not rely on it to "prevent" a render, as this can lead to bugs.
As a rule of thumb, only optimize when needed. But what does that mean?
When to Memoize in React
Now that you know when NOT to memoize, let’s talk about when to use it and how it helps.
First, start by implementing your components and noting any performance issues. The React Devtools is a useful tool to help identify any of these performance issues.
If (and only if) you encounter any performance issues, then you can consider optimizing the offending components using any of the memoization techniques mentioned in the following section.
After the optimization is done, compare the before and after results to see if there is any performance gain.
If the gain is negligible then it’s a probably good idea to discard the memoization changes to avoid introducing bugs or any extra overhead.
How to Implement Memoization in React
Functional components
1. React.memo
A higher-order component that accepts a component and an optional comparison function as its arguments. If provided, the function determines when the component should be updated otherwise it performs a shallow comparison.
This HOC should only be used with pure components.
Use case: Memoizing a pure component to only be rendered if props have changed
// TodoParent.js
const TodoParent = ({ title, description }) => {
const [state, toggleState] = useState(false);
return (
<div>
<Todo title={title} description={description} />
<button onClick={() => toggleState((prevState) => !prevState)}>
Re-render parent
</button>
</div>
);
};
// Todo.js
const Todo = ({ title, description }) => {
return (
<div>
<div>Todo title: {title}</div>
<div>Todo description: {description}</div>
</div>
);
};
export default React.memo(Todo); // Use case 'a' : memoizing a pure component
2. useMemo
useMemo()
is a built-in React hook that accepts 2 arguments. A function that computes a value and a dependencies array.
useMemo
will only recompute the memoized value when one of the dependencies has changed
Use cases:
a. Memoizing an expensive calculation to only be recomputed if dependencies have changed
b. Memoizing a pure component to only be rendered if props have changed
c. Maintaining referential equality for an object value
// TodoParent.js
const TodoParent = ({ title, description }) => {
const [state, toggleState] = useState(false);
return (
<div>
<Todo title={title} description={description} />
<button onClick={() => toggleState((prevState) => !prevState)}>
Re-render parent
</button>
</div>
);
};
// Todo.js
const Todo = ({ title, description }) => {
const computedDescription = useMemo(
() => computeExpensiveDescription(description),
[description]
); // Use case 'a': memoizing an expensive calculation
const MemoizedTodoChild = useMemo(
() => <TodoChild description={description}/>
), [description]); // Use case 'b': memoizing a pure component
const properties = useMemo(() => getPropertiesFromTitle(title), [title]); // Use case 'c': maintaining same reference of properties object
useEffect(() => {
console.log(
"This side effect is called only if properties has changed: ",
properties
);
}, [properties]);
return (
<div>
<div>Todo title: {title}</div>
<div>Todo description: {computedDescription}</div>
{MemoizedTodoChild}// Todo child is a Pure Component that should only get rerendered if description has
changed
</div>
);
};
export default Todo;
3. useCallback
useCallback()
hook is similar to useMemo()
but instead of returning a memoized value, it returns a memoized function
Use cases:
a. Maintaining referential equality of a function
// TodoParent.js
const TodoParent = ({ title, description }) => {
const [state, toggleState] = useState(false);
return (
<div>
<Todo title={title} description={description} />
<button onClick={() => toggleState((prevState) => !prevState)}>
Re-render parent
</button>
</div>
);
};
// Todo.js
import {TodoButton} from ./components/TodoButton
const Todo = ({ title, description }) => {
const onClickHandler = useCallback(() => {
console.log("Todo button clicked");
}, [title]); // Use case: 'a': Maintaining same reference of onClickHandler function
useEffect(() => {
console.log(
"This side effect is only called if onClickHandler has changed"
);
onClickHandler();
}, [onClickHandler]);
return (
<div>
<div>Todo title: {title}</div>
<div>Todo description: {description}</div>
<TodoButton onClick={onClickHandler} /> Todo Button is a Pure Component that should only get rerendered if onClickHandler has changed
</div>
);
};
export default Todo;
How to Memoize Class-based Components:
1. React.PureComponent
React.PureComponent
is the equivalent of React.memo
but for class-based components. React.PureComponent
should also only be used with components that are considered pure components.
Use cases:
a. Memoizing a pure component to only be rendered if props have changed
// Todo.jsx
class Todo extends React.PureComponent { // // Use case 'a' : memoizing a pure component
render() {
return (
<div>
<div>Todo title: {props.title}</div>
<div>Todo description: {props.description}</div>
</div>
);
}
}
export default Todo;
Demo
The example below demonstrates the behavior of memoizing components using React.memo
and useMemo()
The memoized components are expected to get re-rendered only if their props have changed.
Conclusion
Memoization in React is a powerful tool, but it comes with its trade-offs, which is why we recommend avoiding prematurely optimizing everything in React.
React is a performant framework and it is doing a lot of the optimization for you. So, unless needed, further optimization might not be required.
Now you should have a better idea of the many ways to use memoization in React, when you should be making use of it, and when it is probably better to avoid it.
Didn’t quite get the memo?
Fear not! Bitovi has a team of expert React Consultants who are ready to dive in and help with anything from web components to suspense. Book your free consultation to get started!