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.