How I have used useMemo and useCallback in React

Jayanth babu S
Level Up Coding
Published in
9 min readDec 2, 2023

--

useMemo vs useCallback

Welcome to the world of React, a leading JavaScript library for building dynamic user interfaces. If you’re beginning your journey in React development, grasping key concepts like useMemo and useCallback can significantly boost your application's performance. These React hooks play a crucial role in optimizing component rendering, essential for creating efficient, responsive web applications.

In this beginner-friendly guide, we'll demystify the differences between useMemo and useCallback and explain how they enhance your React apps. Perfect for those just starting with React, this article lays out the fundamental differences between useMemo and useCallback in easy-to-understand terms. Let's dive into the world of React performance optimization!

Before diving in, explore more in-depth articles on web development at my personal website:

What is useCallback? 🌱

The useCallback hook is used to memoize functions so that they are not recreated on each render. This can be especially helpful when passing callbacks as props to child components, as it prevents unnecessary re-renders of those components.

The syntax for useCallback is straightforward. It takes two parameters — the function that you want to memoize and an array of dependencies. The function will only be re-created when one or more dependencies in the array change.

let’s take a closer look at how to implement and utilize it effectively.

How to Use useCallback in React 💡

1. Import the useCallback Hook

Start by importing the useCallback hook from React at the beginning of your component file:

import React, { useState, useCallback } from "react";

2. Define the Callback Function

Next, define the callback function that you want to optimize. This is the function that you want to prevent from being recreated on every component re-render.

const Home= () => {
// Define your callback function
const handleClick = () => {
// Function logic here
};

}

3. Memoize the Callback with useCallback

To optimize the callback function, use the useCallback hook. Pass the callback function as the first argument to useCallback. The second argument should be an array of dependencies that the function relies on. These dependencies will trigger the recalculation of the function when they change.

const Home = () => {
// Define your handleClick function
const handleClick = () => {
// Function logic here
};

// Memoize the callback function
const memoizedCallback = useCallback(handleClick, [/* dependencies */]);

return (
<div>
<button onClick={memoizedCallback}>Click Me</button>
</div>
);
}

In the dependency array, list the variables or values that, when changed, should trigger the recreation of the callback function. If the callback doesn’t depend on any values and should remain constant, pass an empty dependency array, [].

When to Use useCallback 🚀

The decision to use useCallback depends on the nature of your application. Here are some scenarios where it's beneficial:

1. Avoiding Unnecessary Re-renders:

When you pass callback functions as props to child components, useCallback can help prevent unnecessary re-renders of those components. By memoizing the function, you ensure that it remains the same between renders.

2. Managing Expensive Computations:

If you have computationally expensive operations within your component, and you want to optimize performance, useCallback can be a valuable tool. It allows you to memoize the computation and recalculate it only when the dependencies change.

Practical Example: TodoList App - Avoiding Unnecessary Re-renders

Consider a scenario where you’re building a to-do list application in React. You have a list of tasks, and each task has a “Delete” button. When a user clicks the “Delete” button, you want to remove the corresponding task from the list. However, you notice that unnecessary re-renders are happening, even for tasks that are not being deleted. This inefficiency can impact the application’s performance.

To solve this problem and optimize the event handling, you can use the useCallback hook.

To-Do List Application without useCallback 🚴

In this version of the to-do list application, we’re not using useCallback. Each time the "Delete" button is clicked, the associated event handler function is recreated, potentially causing unnecessary re-renders. Here's the code:

TodoItem (Child Component)

The TodoItem component represents an individual task in the to-do list. It includes a "Delete" button. When a task is deleted, this component re-renders and logs a message to the console.

// TodoItem.js
import React from "react";

const TodoItem = ({ todo, onDelete }) => {
console.log('child re rendering after delete');
return (
<tr>
<td>{todo}</td>
<td>
<button onClick={() => onDelete(todo)}>Delete</button>
</td>
</tr>
);
};

export default TodoItem;

Component: TodoList 📝

The TodoList component displays a list of tasks using the TodoItem component. When a task is deleted, the child TodoItem components re-render, resulting in multiple console log messages.

// TodoList.js
import React, { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css"; // Add CSS for styling

const TodoList = ({ todos }) => {
const [todoList, setTodoList] = useState(todos);

const handleDelete = (todoToDelete) => {
setTodoList(todoList.filter((todo) => todo !== todoToDelete));
};

return (
<div>
<table className="todo-table">
<thead>
<tr>
<th>Todo</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{todoList.map((todo) => (
<TodoItem key={todo} todo={todo} onDelete={handleDelete} />
))}
</tbody>
</table>
</div>
);
};

export default TodoList;

Component: App.js

// App.js
import React from "react";
import TodoList from "./TodoList";
import "./App.css"; // Add CSS for styling

function App() {
const initialTodos = ["Task 1", "Task 2", "Task 3"];

return (
<div>
<h1>Todo List</h1>
<TodoList todos={initialTodos} />
</div>
);
}

export default App;

In this version, when a task is deleted, the child components re-render, and a message is logged to the console. However, this behavior can lead to inefficiencies in the application’s performance as the number of tasks increases. As a result, users may experience decreased responsiveness and performance degradation in the long run.

Note: In the end, with this code, as the number of tasks in the list grows, the application’s performance may degrade due to unnecessary re-renders of child components, potentially leading to a less responsive user experience.

Demo: 🎈

You can check the demo here

useCallback demo

TodoList Component with useCallback

In this improved version, we’ll utilize the useCallback hook to optimize the event handling. When a task is deleted, the child components no longer re-render, and a message is logged to the console.

With the use of useCallback, the child components representing each task do not re-render when a task is deleted, resulting in improved performance.

In the TodoList component we need to wrap the handleDelete function with useCallback like this:

// Optimized function to delete a task using useCallback
const handleDelete = useCallback(
(todoToDelete) => {
setTodoList((prevTodoList) =>
prevTodoList.filter((todo) => todo !== todoToDelete)
);
},
[]
);

and we wrap the “TodoItem” component with React.memo to memoize it, preventing unnecessary re-renders of this child component when its props don't change.

// TodoItem.js
import React from "react";

const TodoItem = ({ todo, onDelete }) => {
console.log("Child component re-rendered");

return (
<tr>
<td>{todo}</td>
<td>
<button onClick={() => onDelete(todo)}>Delete</button>
</td>
</tr>
);
};

export default React.memo(TodoItem);

With these changes, you optimize the “TodoList” component by using useCallback for the "handleDelete" function and prevent unnecessary re-renders of the "TodoItem" component by wrapping it with React.memo. This leads to improved performance in your React application.

You can check the complete demo here

useCallback without re-rendering

What is useMemo? 🍀

The useMemo hook is used to memoize values or the results of expensive calculations so that they are not re-computed on each render. This can be especially helpful when dealing with computationally intensive operations, and it ensures that the calculated value remains the same between renders, avoiding unnecessary work.

The syntax for useMemo is straightforward. It takes two parameters: the calculation function and an array of dependencies. The calculation function is only re-executed when one or more dependencies in the array change.

Here’s a simple practical example to understand how useMemo works:

Let’s say you have a component that calculates the factorial of a number and displays it. We’ll compare the performance with and without useMemo.

Without useMemo: ⚙️

import React, { useState } from 'react';

function FactorialCalculator() {
const [number, setNumber] = useState(5); // Initial number
const [otherState, setOtherState] = useState('Initial Value');

const calculateFactorial = (num) => {
console.log('calculateFactorial called');
let factorial = 1;
for (let i = 1; i <= num; i++) {
factorial *= i;
}
return factorial;
};

const factorial = calculateFactorial(number);

const handleClick = () => {
// Update the state variable to trigger a re-render
setOtherState('Updated Value');
};

return (
<div>
<div>Factorial of {number} is {factorial}</div>
<div>Other State: {otherState}</div>
<button onClick={handleClick}>Update State</button>
</div>
);
}

In this example, every time you click the “Update State” button to change the otherState variable, it triggers a re-render of the component, which results in the unnecessary re-calculation of the factorial since calculateFactorial is called during every render.

without useMemo in React

With useMemo: 🌱

import React, { useState, useMemo } from 'react';

function FactorialCalculator() {
const [number, setNumber] = useState(5); // Initial number
const [otherState, setOtherState] = useState('Initial Value'); // New state variable

const calculateFactorial = (num) => {
console.log('calculateFactorial called');
let factorial = 1;
for (let i = 1; i <= num; i++) {
factorial *= i;
}
return factorial;
};

const factorial = useMemo(() => calculateFactorial(number), [number]);

const handleClick = () => {
// Update the state variable to trigger a re-render
setOtherState('Updated Value');
};

return (
<div>
<div>Factorial of {number} is {factorial}</div>
<div>Other State: {otherState}</div>
<button onClick={handleClick}>Update State</button>
</div>
);
}
with useMemo

In this example, we use useMemo to memoize the result of calculateFactorial. Now, when you click the "Update State" button to change otherState, it triggers a re-render, but calculateFactorial is only called when the number state changes. This demonstrates how useMemo helps to avoid unnecessary re-calculation and optimizes performance by re-computing the factorial only when the dependent state (number) changes.

Working Demo Codesandbox link here

Tips for useCallback:

  1. Use useCallback when you have callback functions that are passed as props to child components. This can help prevent unnecessary re-renders of child components when the parent component re-renders.
  2. Ensure that the dependencies array passed to useCallback accurately reflects the values that the callback function depends on. This ensures that the function is only recreated when necessary.
  3. Avoid overusing useCallback for all functions in your components. Only optimize the functions that are causing performance issues and re-renders.

Tips for useMemo:

  1. Use useMemo when you have expensive calculations or data transformations that don't need to be recomputed on every render. This can significantly improve performance.
  2. Be mindful of the dependencies array you provide to useMemo. Ensure it accurately represents the values the memoized function relies on. Changing a value that is not listed in the dependencies will not trigger recalculation.
  3. Don’t use useMemo prematurely. Measure the performance of your component first to identify areas where memoization can make a difference.

Conclusion ❤️

Incorporating useCallback and useMemo into your React applications can lead to improved performance and a smoother user experience. These hooks help you avoid unnecessary re-renders and computations, which is essential as your application grows in complexity. By using these optimization techniques wisely, you can ensure that your React application runs efficiently and provides a great user experience.

Happy coding and optimizing your React applications for better performance!

Enjoyed this article? For more in-depth discussions and insights on web development, visit my personal blog at Program With Jayanth.

--

--

https://programwithjayanth.com/ I am a Senior Front-End Engineer with years of experience and specialized in Front-End Web technologies.