How to use the useMemo & useCallback React hook
Markus Persson
Posted on July 2, 2023
Introduction
As your React skills grow, you'll start to care more and more about how well your app performs. When it comes to optimizing React applications, caching is as important as any other tool or programming technique. In React, caching is commonly referred to as memoization, and it is used to reduce the number of times a component is rendered due to state or prop changes. This article will explain how to use the useMemo and useCallback hooks in React for caching purposes.
The default caching in React
By default, React uses a technique called "shallow comparison" to decide whether a component should be re-rendered. It compares the previous props and state of the component with the new ones, and if there are no changes, React assumes that the component hasn't changed, and it won't re-render.
However, when dealing with more complex components that have advanced state management, this default mechanism may not be sufficient. In such cases, additional caching techniques are needed to optimize performance. That's where the useMemo and useCallback hooks come in.
useMemo hook
Note: useMemo hook can only return a value.
When a component renders, all the code within it is executed, including any functions defined within the component. If a function performs a complex calculation, it can be inefficient to execute it on every render, especially if the function's arguments hasn't changed.
React re-renders a component if a prop or state has changed.
This is where the useMemo hook comes into play. By wrapping the function in an useMemo hook, React will remember the calculated value from the function between renders. It will only call the function if any of useMemo's dependencies specified in the dependency array (the second argument of useMemo) has changed. If the dependencies remain the same from the previous render, the previously calculated value is returned, and bypassing the need for a Recalculation.
Example without useMemo:
Here you can see that {expensiveCalculation(count)}
will re-render no matter if your click incrementCount
or incrementCountRender
becuase React re-renders a component if a prop or state has changed. Which results in everything inside of the JSX return()
will get rendered again.
import React, { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const [countRender, setCountRender] = useState(1)
const incrementCount = () => {
console.log('Increment count')
setCount((c) => c + 1);
};
const incrementCountRender = () => {
console.log('Increment count render')
setCountRender((c) => c + 1)
}
return (
<div>
<div>
Count: {count}
<button onClick={incrementCount}>+</button>
<br />
Count render: {countRender}
<button onClick={incrementCountRender}>+</button>
<h2>Expensive Calculation</h2>
{expensiveCalculation(count)}
</div>
</div>
);
};
const expensiveCalculation = (num: number) => {
console.log("Expensive calculation");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
export default App
Example with useMemo:
I removed {expensiveCalculation(count)}
from the JSX return()
and added calculatedResult
function which is wrapped with useMemo and returns expensiveCalculation(count)
with a dependency array that has count
inside of it. This useMemo function will only run in the initial render and if count
has changed.
import React, { useMemo, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const [countRender, setCountRender] = useState(1)
const incrementCount = () => {
console.log('Increment count')
setCount((c) => c + 1);
};
const incrementCountRender = () => {
console.log('Increment count render')
setCountRender((c) => c + 1)
}
const calculatedResult = useMemo(() => {
console.log('useMemo')
return expensiveCalculation(count)
}, [count])
return (
<div>
<div>
Count: {count}
<button onClick={incrementCount}>+</button>
<br />
Count render: {countRender}
<button onClick={incrementCountRender}>+</button>
<h2>Expensive Calculation</h2>
{calculatedResult}
</div>
</div>
);
};
const expensiveCalculation = (num: number) => {
console.log("Expensive calculation");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
export default App
useCallback hook
Note: useCallback hook returns a function.
The useCallback hook in React allows you to memoize a function, ensuring that the function reference remains the same between renders unless any dependencies specified in the dependency array (the second argument of useCallback) have changed.
When a component renders, all the code within it is executed, including the creation of any functions inside the component. If a function is passed down as a prop to child components, it can lead to unnecessary re-renders of those child components if the function reference changes, even if the logic inside the function hasn't changed.
Example without useCallback
The getTodos function retrieves a single todo from the todos array based on the count state. This function is then passed as a prop to the TodosList component, where it is used in an useEffect hook to update the state by spreading the retrieved todo with the existing todos.
This functionality works correctly when there is no countTwo state or setCountTwo button, as they trigger a re-render of the component. If the button is clicked, it will cause the TodosList component to re-render, resulting in the addition of an old todo to the todos state, which can lead to an error.
import React, { useEffect, useState } from "react";
const todos = [
{id: 1, title: 'Todo 1'},
{id: 2, title: 'Todo 2'},
{id: 3, title: 'Todo 3'},
{id: 4, title: 'Todo 4'},
{id: 5, title: 'Todo 5'},
]
const App = () => {
const [count, setCount] = useState(0);
const [countTwo, setCountTwo] = useState(100)
const getTodos = () => {
return todos[count]
}
return (
<div>
<div>
Count: {count}
<button onClick={() => setCount(count + 1)}>+</button>
<br />
Count Two: {countTwo}
<button onClick={() => setCountTwo(countTwo - 1)}>+</button>
<br />
<TodosList getTodos={getTodos}/>
</div>
</div>
);
};
const TodosList = ({getTodos}) => {
const [todos, setTodos] = useState([])
useEffect(() => {
console.log('getTodos function is called.')
setTodos([...todos, getTodos()])
}, [getTodos])
return (
<>
{todos.map(todo => <span key={todo.id}>{todo.title} </span>)}
</>
)
}
export default App
Example with useCallback:
Here, we have wrapped the getTodos function in a useCallback hook. This ensures that the function will only be executed during the initial render of the component or when the count state has changed. Otherwise, it won't be invoked.
import React, { useCallback, useEffect, useState } from "react";
const todos = [
{id: 1, title: 'Todo 1'},
{id: 2, title: 'Todo 2'},
{id: 3, title: 'Todo 3'},
{id: 4, title: 'Todo 4'},
{id: 5, title: 'Todo 5'},
]
const App = () => {
const [count, setCount] = useState(0);
const [countTwo, setCountTwo] = useState(100)
const getTodos = useCallback(() => {
return todos[count]
}, [count])
return (
<div>
<div>
Count: {count}
<button onClick={() => setCount(count + 1)}>+</button>
<br />
Count Two: {countTwo}
<button onClick={() => setCountTwo(countTwo - 1)}>+</button>
<br />
<TodosList getTodos={getTodos}/>
</div>
</div>
);
};
const TodosList = ({getTodos}) => {
const [todos, setTodos] = useState([])
useEffect(() => {
console.log('getTodos function is called.')
setTodos([...todos, getTodos()])
}, [getTodos])
return (
<>
{todos.map(todo => <span key={todo.id}>{todo.title} </span>)}
</>
)
}
export default App
Conclution
useMemo: allows you to calculate and store a value only when its dependencies have changed. By memoizing the value, Then React can avoid unnecessary recalculations and re-renders of the component, improving overall performance.
useCallback: is used to memoize a function. It returns a memoized version of the function that will only be called if its dependencies has been changed. This is particularly useful when passing callbacks as props to child components.
By using useMemo and useCallback wisely, you can optimize your React application by reducing unnecessary calculations and rendering, leading to improved performance and a smoother user experience.
Posted on July 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.