Boosting React Performance: Minimizing Component Re-renders for Faster Rendering
Table of contents
useCallback
is a React Hook that lets you cache a function definition between re-renders - React Docs.
In React applications, it is a standard procedure that whenever the state changes, the corresponding component re-renders to update its state and reflect the changes on the user interface (UI). However, if the component is a parent component that contains other components and also provides data to them, the re-rendering process can cause the child components to recursively re-render as well. This recursive re-rendering can have a significant impact on performance, particularly when dealing with components that display large amounts of data. As a result, this can often hinder the overall application performance.
useCallback, when used appropriately can help to optimize performance on issues relating to component re-rendering.
useCallback(fn, dependencies)
the useCallback, accept two parameters, fn, and dependencies.
Parameters
fn
: The function value that you want to cache. It can take any arguments and return any values.There are a few things to note about this function:
The function is returned and not called during the initial render. On the next renders, React will give you the same function again if the dependencies have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later.
The function is returned to you so you can decide when and whether to call it.
dependencies
: The list of all reactive values referenced inside of thefn
code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. The list of dependencies must have a constant number of items and be written inline like[dep1, dep2, dep3]
.useCallback
caches a function between re-renders until its dependencies change.
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
// Every time the theme changes, this will be a different function...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
By wrapping handleSubmit
in useCallback
, you ensure that it’s the same function between the re-renders (until dependencies change). You don’t have to wrap a function in useCallback
unless you do it for some specific reason. In this example, the reason is that you pass it to a component wrapped in memo
, and this lets it skip re-rendering.
In JavaScript, a function () {}
or () => {}
always creates a different function, similar to how the {}
object literal always creates a new object. Normally, this wouldn’t be a problem, but it means that ShippingForm
props will never be the same, and your memo
optimization won’t work. This is where useCallback
comes in handy - React Docs.
When using memoization (React.memo
) in React to cache a component like ShippingForm
, it helps skip re-rendering when the props remain the same. However, if a prop like handleSubmit
consistently changes on each render without using useCallback
, the ShippingForm
component will be forced to re-render every time.
In this scenario, useCallback
becomes useful. By wrapping the handleSubmit
function with useCallback
, you can ensure that the function is not recreated on each render unless the reactive values in its dependencies change. This prevents unnecessary re-renders of the ShippingForm
component when handleSubmit
itself hasn't changed.
Here's an example of how to use useCallback
with ShippingForm
:
import React, { useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
import { useState } from 'react';
import ProductPage from './ProductPage.js';
export default function App() {
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<ProductPage
referrerId="wizard_of_oz"
productId={123}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
toggling the checkbox and changing the theme
state from false to true will trigger a re-render of the App
component. If the child components are not memoized with React.memo
and useCallback
, it will result in a recursive re-rendering of all the child components.
In scenarios where the App
component has a large number of child components, this recursive re-rendering can indeed lead to delays and impact performance, especially in live applications where real-time updates are crucial.
import ShippingForm from './ShippingForm.js';
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
// Every time the theme changes, this will be a different function...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
In the scenario where the handleSubmit does not useCallback, the handlesubmit
function changes whenever the theme
state changes, the ShippingForm
component will re-render. To avoid unnecessary re-renders of the ShippingForm
component in this situation, you can use useCallback
to memoize the handleSubmit
function and React.memo
to memoize the ShippingForm
component itself.
By memoizing the handleSubmit
function with useCallback
and passing its dependencies, you ensure that the function is only recreated when those dependencies change. This helps prevent unnecessary re-renders of the ShippingForm
component if the theme
state changes without affecting the dependencies of handleSubmit
.
Using useCallback
in combination with React.memo
can help optimize the performance of your React components by preventing unnecessary re-renders when props remain the same.
- Caveats
useCallback
is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.function ReportList({ items }) { return ( <article> {items.map(item => { // 🔴 You can't call useCallback in a loop like this: const handleClick = useCallback(() => { sendReport(item) }, [item]); return ( <figure key={item.id}> <Chart onClick={handleClick} /> </figure> ); })} </article> ); }
Instead, extract a component for an individual item, and put useCallback
there:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Call useCallback at the top level:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Absolutely! When dealing with large amounts of data in a React application, fetching and displaying that data efficiently is crucial for optimal performance.
In such scenarios, useCallback
can indeed be a valuable tool for caching functions and preventing unnecessary re-renders. By memoizing functions with useCallback
, you can ensure that the function instances are cached and reused, reducing the overhead of recreating functions on each render.
This is particularly beneficial when working with components that rely on callbacks or event handlers, as it helps prevent unnecessary re-renders of components that use these functions as props. By memoizing the functions, you ensure that the components only re-render when their dependencies change.
Additionally, useCallback
plays well with other optimization techniques like memoization (e.g., using React.memo
) and selective rendering (e.g., using pagination or virtualization) to further enhance the performance of data-intensive applications.
By combining these optimization strategies and leveraging the benefits of useCallback
, you can significantly improve the speed and efficiency of fetching and displaying large amounts of data in your React application.
Subscribe to my newsletter
Read articles from Blessed Dominic directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Blessed Dominic
Blessed Dominic
Experienced software engineer with a passion for developing innovative solutions