7/29/2023
React has revolutionized the way we build interfaces with its innovative and dynamic approach.
Ever since version 16.8 rolled out, Hooks have become a game-changer, enabling developers to work with state and other React features without the need for classes.
Among these Hooks, two stand out for their significance: useCallback and useMemo.
In this article, we'll take a deep dive into these hooks, understand their differences, and learn when, why, and how they should (or should not) be used.
A Brief Introduction to useCallback and useMemo
The useCallback hook returns a memoized version of the callback function that only changes if one of the dependencies has changed. It's useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
The useMemo hook returns a memoized value. Like useCallback, it only re-calculates the memoized value when one of the dependencies has changed. It's great for expensive calculations.
When and How to Use useCallback and useMemo
Now that we have a brief understanding, let's jump into some practical examples.
An Intricate Game Scenario - Using useCallback
Imagine we're creating an intricate game application where a player can increase their level by collecting gems. Every time they level up, they get a celebratory message. Here's a simplified version of that.
import React, { useState, useCallback } from 'react';
function Game() {
const [level, setLevel] = useState(1);
const levelUp = () => {
setLevel(level + 1);
};
return <Board level={level} onLevelUp={levelUp} />;
}
function Board({ level, onLevelUp }) {
// A heavy computation function that renders the game board
renderBoard(level);
return (
<div>
<h2>Current Level: {level}</h2>
<button onClick={onLevelUp}>Collect a gem</button>
</div>
);
}
Now, the issue here is that the levelUp function is created each time Game renders. Thus, Board re-renders every time, even when there are no level changes. This might slow down our app, especially with complex board rendering. Here's where useCallback shines:
import React, { useState, useCallback } from 'react';
function Game() {
const [level, setLevel] = useState(1);
const levelUp = useCallback(() => {
setLevel(level + 1);
}, [level]);
return <Board level={level} onLevelUp={levelUp} />;
}
With this change, a memoized levelUp function is passed to Board unless the level changes. The expensive board rendering process only happens when necessary, improving performance.
An E-Commerce Filter - Using useMemo
Suppose you're building an e-commerce app with a product listing page. There's a filter feature that allows users to search for products by their names. Here's a basic setup:
import React, { useState } from 'react';
function ProductList({ products }) {
const [filter, setFilter] = useState('');
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<>
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
/>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</>
);
}
The issue is, the filter function runs each time ProductList renders. If you have thousands of products, this could slow down your app significantly. The useMemo hook can solve this problem:
import React, { useState, useMemo } from 'react';
function ProductList({ products }) {
const [filter, setFilter] = useState('');
const filteredProducts = useMemo(() =>
products.filter(product =>
product.name.toLowerCase().includes(filter.toLowerCase())
), [products, filter]);
return (
// ...
);
}
Now, the expensive filter function only runs when products or filter changes, leading to much better performance for your product list.
When Not to Use useCallback and useMemo
While useCallback and useMemo can provide performance boosts in specific scenarios, they should not be used everywhere. Here's why.
Overuse of useCallback
Using useCallback unnecessarily can lead to more harm than good. It creates an overhead of maintaining the memoized version of the function, which can be more expensive than the function invocation itself. Let's consider an example:
import React, { useState, useCallback } from 'react';
function Greeting() {
const [name, setName] = useState('');
const updateName = useCallback((event) => {
setName(event.target.value);
}, []);
return (
<input
type="text"
value={name}
onChange={updateName}
/>
);
}
In this example, useCallback is not needed, because the updateName function is not computationally expensive or passed as a prop causing unnecessary re-renders. Removing useCallback from this code simplifies it without any performance downside.
Misuse of useMemo
useMemo can also be overused or misused. If the calculation isn't computationally expensive, then useMemo might bring more overhead than benefits. For example:
import React, { useMemo } from 'react';
function TotalPrice({ quantity, price }) {
const totalPrice = useMemo(() => quantity * price, [quantity, price]);
return (
<h2>Total Price: {totalPrice}</h2>
);
}
In this case, useMemo is unnecessary since the multiplication operation isn't expensive. It would be better to simply calculate totalPrice without memoization:
import React from 'react';
function TotalPrice({ quantity, price }) {
const totalPrice = quantity * price;
return (
<h2>Total Price: {totalPrice}</h2>
);
}
Conclusion
useCallback and useMemo are powerful tools in the React developer's toolkit. They are designed to help optimize React apps by preventing unnecessary renders and computations.
However, like many powerful tools, they can lead to issues when used inappropriately or excessively. By understanding the use cases for these hooks and using them judiciously, we can create more performant and maintainable React applications.