React's re-rendering process is necessary to keep the user interface up-to-date with the latest data, but it can also be a performance bottleneck. If components re-render too frequently, it can slow down the application and make it feel unresponsive. This article will show several ways to avoid needless re-rendering for a smoother, faster user interface.
React rendering refers to the process of updating the user interface (UI) of a React application to show the changes in its underlying state or props. When a component's state or props change, React automatically re-renders the component, i.e., it calls the component's render method again to generate a new representation of the user interface.
In this article, we will explain how to use the useMemo
, useCallback
, and useRef
hook to optimize expensive calculations, avoid unnecessary re-renders, and store values that don't need to trigger re-renders. By the end of this article, you'll better understand how to improve your React applications' performance using these powerful React hooks.
Using React.useMemo
The useMemo
hook can prevent unnecessary re-renders by memoizing the result of a function and only recomputing it if its dependencies have changed.
Let's take a look at the example below:
import React, { useState, useMemo } from 'react';
function calculateTotal(items) {
console.log('calculating total');
return items.reduce((total, item) => total + item.value, 0);
}
function App() {
const [items, setItems] = useState([
{ id: 1, value: 10 },
{ id: 2, value: 20 },
{ id: 3, value: 30 },
]);
const total = useMemo(() => calculateTotal(items), [items]);
function handleClick() {
setItems([
{ id: 1, value: 10 },
{ id: 2, value: 20 },
{ id: 3, value: 40 },
]);
}
return (
<div>
<p>Total: {total}</p>
<button onClick={handleClick}>Update</button>
</div>
);
}
From the above example, we initialize a state named items
, representing an array of objects with id and value properties. The calculateTotal
function is used to sum up the values of all the items in the array.
The total
variable is computed using useMemo
, which takes a function that returns the memoized value, and an array of dependencies. In this case, we only pass items
as the array. The memoized value is only recalculated if any of the dependencies have changed. In this case, the only dependency is the items array, so calculateTotal
will only be called again if the items array has changed.
When the handleClick
function is called, it updates the items state to include a new item with a value of 40. Say we did not use the useMemo
hook; the entire component would re-render, even though the total value hasn't changed. But because we're using useMemo
, calculateTotal
is only called once again, resulting in better performance.
useMemo
is a useful hook for optimizing performance in React by preventing unnecessary re-renders.
NB: Memoization should only be used for expensive calculations, as it adds some overhead to the rendering process.
React.useRef
The second way to optimize performance in React applications by preventing unnecessary re-renders is using the useRef
hook, which can prevent unnecessary re-renders by storing a mutable value that persists across renders.
Below is an example of how useRef
can be used to prevent unnecessary re-renders:
import React, { useRef, useState } from 'react';
function App() {
const [name, setName] = useState('');
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
ref={inputRef}
/>
<button onClick={handleClick}>Focus</button>
</div>
);
}
Here, we have a state stored as name
, which stores the value of an input field. The inputRef
variable is created using the useRef
hook, which returns a mutable ref object that can be used to store any value.
The inputRef
ref object is then passed to the input element as a ref prop. This allows us to directly access the underlying DOM node using inputRef.current
.
In the handleClick
function, we use inputRef.current.focus()
to programmatically focus the input field when the button is clicked.
If we did not use the useRef
hook to store the inputRef
value, we would have to re-render the entire component every time the handleClick
function is called just to get access to the input field. But because we're using useRef, the inputRef value persists across renders, preventing unnecessary re-renders and improving performance.
Also, useRef
should only be used for mutable values that don't affect the component's rendering. If a mutable value does affect the rendering of the component, it should be stored in the state instead.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay โ an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
React.useCallBack
The useCallBack
hook memoizes functions so they are not recreated on each component render. When memoized with useCallback
, a function will only be created once and reused on subsequent renders if the function's dependencies remain the same.
Let's look at an example of how the useCallBack
hook can help prevent unnecessary re-renders.
import React, { useState } from 'react';
function App({ items }) {
const [visibleItems, setVisibleItems] = useState([]);
const toggleItemVisibility = (itemId) => {
if (visibleItems.includes(itemId)) {
setVisibleItems(visibleItems.filter((id) => id !== itemId));
} else {
setVisibleItems([...visibleItems, itemId]);
}
};
return (
<div>
{items.map((item) => (
<div key={item.id}>
<h2>{item.title}</h2>
<button onClick={() => toggleItemVisibility(item.id)}>
{visibleItems.includes(item.id) ? 'Hide' : 'Show'}
</button>
{visibleItems.includes(item.id) && <p>{item.content}</p>}
</div>
))}
</div>
);
}
Assuming we have the above component that shows the list of items, users can toggle the visibility of each item displayed. The toggleItemVisibility
function is created inside the component and passed as a callback to each button's onClick
event. However, since toggleItemVisibility
depends on the state variable visibleItems
, it will be recreated on every render, causing unnecessary component re-renders.
To prevent this, we can make use of the useCallBack
hook by memoizing the toggleItemVisibility
function like below:
import React, { useState, useCallback } from 'react';
function ItemList({ items }) {
const [visibleItems, setVisibleItems] = useState([]);
const toggleItemVisibility = useCallback((itemId) => {
if (visibleItems.includes(itemId)) {
setVisibleItems(visibleItems.filter((id) => id !== itemId));
} else {
setVisibleItems([...visibleItems, itemId]);
}
}, [visibleItems]);
return (
<div>
{items.map((item) => (
<div key={item.id}>
<h2>{item.title}</h2>
<button onClick={() => toggleItemVisibility(item.id)}>
{visibleItems.includes(item.id) ? 'Hide' : 'Show'}
</button>
{visibleItems.includes(item.id) && <p>{item.content}</p>}
</div>
))}
</div>
);
}
Here, the visibleItems
state is being passed as the second argument to the useCallback
hook,ensuring that the toggleItemVisibility
function is only recreated when the visibleItems
state variable changes. This prevents unnecessary re-renders of the component and improves performance.
Conclusion
In this tutorial, we have learned what React re-rendering is, how it can cause bottlenecks in React applications, and ways of optimizing unnecessary re-renders. Generally, React provides several hooks that can be used to optimize the performance of a React application. These hooks provide a way to store values and functions between renders, which can significantly improve the performance of a React application.