Skip to main content
👀 Interested in the latest enterprise backend features of refine? 👉 Join now and get early access!
Memoization in React - How useCallback Works
Fullstack Developer
6 min read

Memoization in React - How useCallback Works

Introduction

This post is about using the useCallback() hook in React. This is the third part of the series titled Memoization in React.

In React, callback functions like event handlers inside a component are re-created as unique function objects at every re-render of the component. When a callback is passed from a parent to a child as a prop, the child component re-renders just because of the absence of referential integrity of the callback. useCallback() helps maintain the callback's referential integrity and prevent these re-renders.

We'll look at the details of the useCallback() hook with an example demonstrated in the previous post titled Memoization in React - How useMemo() Works.

Steps we'll cover:

Project Content Overview

The app is based on the idea of a list of posts on a blog.

The example app live code

The discussion of this article is focused on optimizing performance by memoizing callback functions that are passed as a prop from a parent component to a child. We are going to use the useCallback() hook for this.

Pass Callback from Parent to Child

In this example, we'll consider the <UserPostsIndex />, <UserPostsList /> and <UserPosts /> components.

As you can see below, <UserPostsIndex /> fetches and sets userPosts when the compnent renders:

components/UserPostIndex.jsx
import React, { useEffect, useState } from "react";
import fetchUserPosts from "../fetch/fetchUserPosts";
import UserPostsList from "./UserPostsList";

const UserPostsIndex = ({ signedIn }) => {
const [userPosts, setUserPosts] = useState([]);

const deletePost = e => {
const { postId } = e.currentTarget.dataset;
const remainingPosts = userPosts.filter(post => post.id !== parseInt(postId));
setUserPosts(remainingPosts);
};

useEffect(() => {
const posts = fetchUserPosts();
setUserPosts(posts);
}, []);


return (
<div className="my-1 p-2 box">
<div className="m-1 py-1">
<h2 className="heading-md">Your Posts</h2>
<p className="m-1 p-1">{signedIn ? `Signed in`: `Signed out `}</p>
{
userPosts &&
(
<div className="px-1">
{
<UserPostsList userPosts={userPosts}
deletePost={deletePost}
/>
}
</div>
)
}
</div>
</div>
);
};

export default React.memo(UserPostsIndex);

In the JSX, we have a deletePost() function passed on to <UserPostsList /> component, along with userPosts. Prior to that, <UserPostsIndex> receives signedIn as a prop from <Blog />.

In the <UserPostsList /> component, we receive userPosts and deletePost() function and display a <UserPost /> for each post in userPosts array:

components/UserPostList.jsx
import React from 'react';
import UserPost from './UserPost';

const UserPostsList = ({ userPosts, deletePost }) => {

console.log('Rendering UserPostsList component');

return (
(
<div className="px-1">
{
userPosts.map(post => (
<div key={post.id} className="my-1 box flex-row">
<UserPost post={post} />
<button className="btn btn-danger" data-post-id={post.id} onClick={deletePost}>Delete</button>
</div>
))
}
</div>
)
);
};

export default React.memo(UserPostsList);

Inside <UserPostsList />, deletePost() is used to remove an item from the list.

<UserPost /> is just a presentational component where we display the title as a link. Feel free to go over it in your editor.

Now let's add following:

components/UserPostIndex.jsx
console.log('Rendering UserPostsIndex component');

and this one in <UserPostsList />:

components/UserPostList.jsx
console.log('Rendering UserPostsList component');

If we check our console, we can see the logs for the inital rendering of the components.
Then if we click the SignOut button on the navbar, we see batches of renders from <UserPostsIndex />, <UserPostsList /> and <UserPost />:

usecallback1

We can account for the re-render of <UserPostsIndex /> because the value of the signedIn prop changed. However, re-rendering <UserPostsList /> does not make sense because userPosts does not change with the change in the value of signedIn.

We already memoized <UserPostsList /> with React.memo(). We don't see any reason why it should re-render due changes in any ancestor's state.

Does deletePost() change ?



Referential Integrity

Well, deletePost() changes, and it changes due to breaking of referential integrity. And this change triggers a re-render in <UserPostsList /> which we don't expect to see.

In React, a function passed to a component as a prop fails to maintain referential integrity because a new function object is created at every render from a function declared inside a parent component. So the value of the prop in the receiver component is different at every re-render of the parent. As a result, the receiver also re-renders, unexpectedly.

In our example, <UserPostsList /> is not expected to be re-rendered, but it does because referential integrity fails as deletePost() is a different function object at every re-render of <UserPostsIndex />, i.e. they are not equal. So, when we change signedIn, <UserPostsIndex /> re-renders, and so <UserPostsList /> also re-renders.

Memoizing Event Listeners with useCallback()

Now, memoizing deletePost() produces the same function at every re-render. Let's memoize it with useCallback() by making the following changes in <UserPostsIndex />:

components/UserPostIndex.jsx
import React, { useCallback, useEffect, useState } from "react";

const UserPostsIndex = ({ signedIn }) => {

const deletePost = useCallback(e => {
const { postId } = e.currentTarget.dataset;
const remainingPosts = userPosts.filter(post => post.id !== parseInt(postId));
setUserPosts(remainingPosts);
}, [userPosts]);

...

};

export default React.memo(UserPostsIndex);

Now, if we click the Sign Out button a few times, we'll see in the console that <UserPostsIndex /> is re-rendered, but <UserPostsList /> and <UserPost /> is not:

usecallback2

This is because useCallback() caches and produces the same copy of deletePost() at every render of <UserPostsList />, until its dependencies change.

Here, a change in userPosts triggers renewal of the memo of the function, so everytime the value of userPosts changes, <UserPostsList /> will be re-rendered.


discord banner

Other Cases

Memoized callbacks are very important to maintain referential integrity so that the same function object is made available every time a component re-renders. useCallback() is also used to cache callbacks in debouncing, as well as dependencies in hooks like useEffect().

Conclusion

In this article, we looked at how re-renders of a parent component lead to violation of referential integrity of callback functions passed in as props to child components, which causes unnecessary re-renders of a child. useCallback() helps us produce the same function object at every re-render of the parent, and be pass it to the child. This prevents the unnecessary re-renders of child components.


discord banner

Example

Run on your local
npm create refine-app@latest -- --example blog-react-memoization-memo

Related Articles

How to use Material UI Tooltip

We'll discover the Material UI Tooltip component with examples

How to use React Strict Mode in React 18

What is Strict Mode in React 18 and how to use it to find and fix bugs in your React application.

refine vs React-Admin Which is Better for Your Project?

We will compare the features of refine and react-admin