Lately, I have been working on some old React applications. During this time I learnt and implemented some techniques to optimize and improve the performance of a React application. One such technique is Memoization.
Here in this blog, I will talk about how to apply Memoization in a React application. We will see how the React.memo()
function can be used to avoid unnecessary re-renders. We will also check the useCallback
and useMemo
hooks and how to use them alongside the React.memo()
higher-order function. In the end, we will discuss practical use cases on when to apply memoization and when to skip.
In technical terms -
Memoization in software engineering is a technique of caching the results of expensive operations and reusing them whenever the same operation is called. Using this technique, redundant time-consuming operations are skipped, thus improving performance.
Before we move on to the actual topic, let’s revise some basics first.
React is a JavaScript library used for building user interfaces. It follows a component-based architecture, where the UI is divided into small, reusable components that function independently based on their state.
A state can be thought of as the local memory of a component, responsible for holding its dynamic data. React ensures the UI stays up-to-date by re-rendering components whenever the state of that component changes. In essence, any change in a component’s state triggers a re-render to reflect the updated change on the UI.
A Component in React will re-render on two occasions
- If the state of that component changes.
- If the Parent component of that component re-renders.
For example, if we have Component A, which is a parent of Component B, then every time Component A re-renders, B will automatically re-render itself since it is the child of that Component. See the image below for a better understanding.
By triggering a component re-render, React maintains the sync between all the components and keeps the UI updated with the latest changes. But this very own re-rendering property of React also is responsible for slowing down the application. You see, re-rendering a component means re-creating the entire Component along with the variables and functions inside it from scratch. This process is not always simple and inexpensive. For example, look at the below image
When Component A re-renders, all components within its branch(child components) will also be re-rendered. This means that even if a component didn't undergo state change, it will still be re-rendered again since its parent got re-rendered. This at times is unnecessary and inefficient. So as per the above image, if Component A re-renders, Component B, C, and D will be re-rendered.
If these components involve resource-intensive operations such as data fetching, initializing large objects or arrays, or running computationally expensive functions, those operations will be executed again. This situation becomes even more problematic if Component X triggers a re-render. In such a case, not only will the entire Branch A(children of A) re-render, but so will Branch B(children of B), causing significant performance issues.
Hence, the inherent behavior of React, which involves re-rendering, can also contribute to performance degradation. Now that we have identified the problem, let's explore how we can address it effectively.
Enough talk, let’s start writing some code. In the below code, I have written a simple Parent component with a count
state, a variable displaying the count
value, and a <button>
to update the count. Inside the Parent component, I have added a simple Child component that takes no props.
import { useState } from 'react'
function Child() {
console.log("Child component rendered");
return (
<p>This is child component</p>
)
}
export default function Parent() {
const [count, setCount] = useState(0);
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<Child />
</div>
</>
)
}
After running the above code in the browser we can see the below output in the browser's console.
Try clicking the <button>
to change the count. If you see the console again, you will observe that not only the Parent component is re-rendered but also the Child component.
This is the unnecessary re-rendering of the Child component problem that we discussed earlier.
Component rendering can be a heavy task that might affect the overall performance of an application. To avoid this unnecessary re-rendering of components, we can use the React.memo()
higher-order function.
What is React.memo()
:
React.memo()
is a higher-order component provided by React to create a memoized version of a component. A memoized component makes a shallow comparison of the previous and new props. If the props are different, the components re-render while if the props are the same the component re-rendering is skipped. If props are absent, the component does not re-render on the Parent component re-render. Let’s see this in action by modifying the previous non-performant code.
import React, { useState } from 'react'
function Child() {
console.log("Child component rendered");
return (
<p>This is child component</p>
)
}
//creating a memoized Component
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0)
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild />
</div>
</>
)
}
Now re-run the application and open the console. The Parent and Child component will be re-rendered on the initial render. After that every time you click on the <button>
, only and only the Parent Component will re-render and rendering of the Child component will be skipped.
Well, that's how easy it is to memoize a component. Let's try some more scenarios with the same example and see how memoization performs in those cases.
In the previous code, I added a new variable called user
of type string
and passed it as a prop
to the Child component.
import React, { useState } from 'react'
//passing user as a prop to Child
function Child({user}) {
console.log("Child component rendered");
return (
<>
<p>{user}</p>
</>
)
}
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0);
const user = 'joe';
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild user={user} />
</div>
</>
)
}
Now, if you rerun the application and press the count button, the Child component will not re-render. This is because we have memoized it. Every time the Parent component re-renders, the memoized Child component does the prop check. Since the user prop does not change, the Child component does not re-render.
Now, I will change the type of user
variable to an object and run the same scenario.
import React, { useState } from 'react'
function Child({user}) {
console.log("Child component rendered");
return (
<>
<p>{user.name}</p>
<p>{user.email}</p>
</>
)
}
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0);
const user = {
name: 'joe',
email: 'joe@email.com'
};
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild user={user} />
</div>
</>
)
}
Now after the initial render, if I click on the count button, the Child component starts re-rendering again! The memoization fails in this case.
But why? We memoized the function and the value of the user
variable also does not change so why does the Child component re-render? Well, this is because React.memo()
does a shallow comparison of the props that are passed to the component. Props of types String, Number, and Boolean are passed by value whereas props of type Arrays, and Objects are passed by reference.
var a = 'joe';
var b = 'joe';
a === b //true
var objA = {
name: 'joe'
}
var objB = {
name: 'joe'
}
objA === objB //false
If you do not understand the above code, I suggest you read about how Objects and Arrays work in JavaScript.
So what's happening in our React code is the user variable is of type Object. In the initial render, it is initialized with an initial reference. When the count
state is updated in the Parent Component, the Parent component re-renders and hence the user
object is initialized again with a new reference. So now the memoized Child component does a shallow check i.e. it checks the reference of the object rather than the actual object values. Since the reference has changed after the initial render it assumes that the variable has been updated and hence the Child component gets rendered again!
Hence, to avoid re-rendering, we need to preserve the reference to the user object between the renders. In such scenarios, the useMemo()
hook can be helpful.
The useMemo()
hook is used to memoize a value and preserve its reference between component re-renders. This hook takes an array of dependencies as a second parameter and recalculates the value if any of the dependencies change. In our case, we can use this hook to memoize the user object. Let's update the code with the useMemo()
hook.
import React, { useMemo, useState } from 'react'
function Child({user}) {
console.log("Child component rendered");
return (
<>
<p>{user.name}</p>
<p>{user.email}</p>
</>
)
}
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0);
const user = useMemo(() => (
{
name: 'joe',
email: 'joe@email.com'
}
), []);
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild user={user} />
</div>
</>
)
}
Now the user
object will not re-initialized and lose its reference between renders and therefore the Child component will not re-render whenever the Parent Component re-renders.
The useMemo()
hook can also be used to memoize a returned value from a heavy function. The value is saved locally and only re-calculated if any of its dependencies change. Take a look at the following example
import React, { useMemo, useState } from 'react'
export default function Home() {
const [count,setCount] = useState(0)
const [age,setAge] = useState(24)
function handleCount(){
setCount(count+1)
}
function handleAge(){
setAge(age+1)
}
//use the useMemo hook to memoize the memoizedDays value and only re-run //the getDays function if age changes
const memoizedDays = useMemo(function getDays(){
console.log("get days")
return age * 365
},[age])
return (
<div>
<h1>Count is {count}</h1>
<button onClick={handleCount}>Add Count</button>
<h1>Age is {age}</h1>
<button onClick={handleAge}>Increment Age</button>
<div>Get days {memoizedDays}</div>
</div>
)
}
You can read more about the useMemo()
hook here..
Now let's pass one more prop to the Child component, this time a function.
import React, { useMemo, useState } from 'react'
function Child({user, handleLogout}) {
console.log("Child component rendered");
return (
<>
<p>{user.name}</p>
<p>{user.email}</p>
<button onClick={handleLogout}>Logout User</button>
</>
)
}
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0);
const user = useMemo( () => (
{
name: 'joe',
email: 'joe@email.com'
}
), []);
const handleLogout = () => {
console.log('Logging out user');
}
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild user={user} handleLogout={handleLogout} />
</div>
</>
)
}
Now if you check in the browser, the Child will start again re-rendering on the count
state change.
The reason behind re-rendering is similar to what we discussed in the previous example. Similar to objects, functions are also passed by references. Hence, when the Parent component re-renders, the reference of the function is changed and hence the memoized child Component re-renders again.
To avoid this, we can use the useCallback()
React hook which is similar to the useMemo()
hook. The useCallback()
hook is used to optimize callback functions passed to child components. The only difference is useCallback()
hook is used to memoize functions whereas the useMemo()
hook is used to memoize values. You can read more about the useCallback()
hook here.
import React, { useCallback, useMemo, useState } from 'react'
function Child({user, handleLogout}) {
console.log("Child component rendered");
return (
<>
<p>{user.name}</p>
<p>{user.email}</p>
<button onClick={handleLogout}>Logout User</button>
</>
)
}
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0);
const user = useMemo( () => (
{
name: 'joe',
email: 'joe@email.com'
}
), []);
const handleLogout = useCallback(() => {
console.log('Logging out user');
}, []);
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild user={user} handleLogout={handleLogout} />
</div>
</>
)
}
In the above code, we used the useCallback()
hook to avoid the re-rendering of the Child Component by preserving the function reference. Now every time, the Parent component re-renders, the handleLogout()
function will not lose its reference and hence the Child component will not re-render.
As you can see, only the Parent component gets re-rendered when the count
state changes.
With that, we have completed all the scenarios that you might encounter while trying to implement Memoization in your React app. While Memoization is great for improving performance, if not used correctly it can negatively affect performance.
Let's understand this by an example. I have used the same code from our previous example where we are memoizing the user
and handleLogout
props and passing them to the memoized Child component.
import React, { useCallback, useMemo, useState } from 'react'
function Child({user, handleLogout}) {
console.log("Child component rendered");
return (
<>
<p>{user.name}</p>
<p>{user.email}</p>
<button onClick={handleLogout}>Logout User</button>
</>
)
}
const MemoizedChild = React.memo(Child);
export default function Parent() {
const [count, setCount] = useState(0);
const user = useMemo( () => (
{
name: 'joe',
email: 'joe@email.com'
}
), []);
const handleLogout = useCallback(() => {
console.log('Logging out user');
}, []);
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<MemoizedChild user={user} handleLogout={handleLogout} />
</div>
</>
)
}
As you can see, we had to add quite a few extra lines of code and made the code complex just to memoize a simple Component. Let's look at the un-memoized version of the above code.
import { useState } from 'react'
function Child({user, handleLogout}) {
console.log("Child component rendered");
return (
<>
<p>{user.name}</p>
<p>{user.email}</p>
<button onClick={handleLogout}>Logout User</button>
</>
)
}
export default function Parent() {
const [count, setCount] = useState(0);
const user = {
name: 'joe',
email: 'joe@email.com'
};
const handleLogout = () => console.log('Logging out user')
console.log("Parent component rendered");
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<div>
<Child user={user} handleLogout={handleLogout} />
</div>
</>
)
}
By simplifying our code, we reduced unnecessary complexity and eliminated a few extra lines. In software engineering, every line of code contributes to memory usage and can impact application performance, even if marginally. In our example, the Child component is simple and does not involve heavy computations. As a result, rendering this component has minimal impact on the application's overall performance. In such scenarios, adding an extra layer of complexity through memoization is unnecessary and does not justify the effort, as it offers no meaningful performance gains.
When I think about optimizing anything, I always follow the following principle.
"The cost of optimization should not outweigh the actual benefit from optimization".
This means, that while performing any sort of optimization, always make sure that the code you write doesn’t introduce unnecessary complexity or maintenance overhead that outweighs the benefits of the optimization itself. So next time you think about optimizing something, think if the performance boost after optimizing is worth the effort and extra code that you write.
So then when should we actually consider Memoization?
Here is a very good example for it.
function PostComponent({post}) {
console.log(`Post Component - ${post.id} rendering`);
function getPostDetails(post) {
console.log(`getPostDetails - Fetching Post details for Post ${post.id}`);
// Simulate a fetch request
const res = `Fetching details for Post ${post.id}`;
return res;
}
// Call getPostDetails directly in the render
const postDetails = getPostDetails(post);
return (
<div>
<p>{post.title}</p>
</div>
)
}
export default function App() {
const [posts, setPosts] = useState([
{
id: 1,
title:"Post 1"
},
{
id: 2,
title:"Post 2"
},
{
id: 3,
title:"Post 3"
},
{
id: 4,
title:"Post 4"
}
]);
const handlePost = () => {
//consider expensive calcuations happening here
console.log('Logging out user')
};
const [count, setCount] = useState(0);
console.log('App Component rendered');
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
{posts.map((post) => <PostComponent key={post.id} handlePost={handlePost} post={post} />)}
</div>
</>
)
}
In the above piece of code, we have an <App/>
component that has a list of posts
and a count
state. Inside the App component, we are rendering the list using the <Post/>
component and passing the handlePost
and post
props. The <Post/>
component calls a function to get details of a post.
In this case, memoizing is essential because we have a long list of posts. So every time the count
changes and the <App />
component re-renders, all these <Post/>
components will also render and the function getPostDetails()
will be called again. If you see the below picture, you can notice that all the <Post />
components get re-rendered again.
Let's memoize this code!
function PostComponent({post}) {
console.log(`Post Component - ${post.id} rendering`);
function getPostDetails(post) {
console.log(`getPostDetails - Fetching Post details for Post ${post.id}`);
// Simulate a fetch request
const res = `Fetching details for Post ${post.id}`;
return res;
}
// Call getPostDetails directly in the render
const postDetails = getPostDetails(post);
return (
<div>
<p>{post.title}</p>
</div>
)
}
const MemoizedPostComponent = React.memo(PostComponent);
export default function App() {
const [posts, setPosts] = useState([
{
id: 1,
title:"Post 1"
},
{
id: 2,
title:"Post 2"
},
{
id: 3,
title:"Post 3"
},
{
id: 4,
title:"Post 4"
}
]);
const handlePost = useCallback(() => {
//consider expensive calcuations happening here
console.log('Logging out user')
}, []);
const [count, setCount] = useState(0);
console.log('App Component rendered');
return (
<>
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
{posts.map((post) => <MemoizedPostComponent key={post.id} handlePost={handlePost} post={post} />)}
</div>
</>
)
}
Observe the optimizations in the output below.
As you can see, on the count
change only the <App/>
component re-rendered and all the <Post/>
components skipped re-rendering. This is the perfect example where Memoizing will help improve the performance!
Best Practices
Before we end this blog, let's look at some best practices for React Memoization
- Skip memoizing Static Component or Components whose props rarely change.
- Use
React.memo()
judiciously. If the props for your components change regularly, Memoizing is not needed there. - Use the
useCallback()
anduseMemo()
hooks for memoizing non-primitive types of props like Objects, Arrays, or Functions. - Use the React profiler tool to monitor the performance of your Application.
- Always evaluate the efforts for Optimization against the benefits.
- The
React.memo()
function only performs shallow comparison of props, if you want to perform deep comparison, use a custom comparator function. A custom comparator function is passed as a second parameter to theReact.memo()
function in cases where custom comparison between old and new props is required. If this function returnstrue
, the Memoized Component will re-render and will skip re-rendering if the function returnsfalse
.
const MemoizedPost = React.memo(PostComponent, (prevProps, nextProps) => {
if (deepCompare(prevProps, nextProps)) {
return true;
} else {
return false
}
})
Wrapping Up
That's all we had for Memoization in React. In this blog, we understood how Memoization works in React with the help of the React.memo()
, useCallback()
and useMemo()
hook. We saw a few scenarios on handling Memoization as well as a few examples of when to use Memoization and when to skip. In the end, we saw a few best practices when it comes to Memoization!
I hope you liked this blog and learned something new! I share such development-related tips on my Twitter as well. Happy coding!
Top comments (0)