2024-09-26 Web Development
Optimizing React Components with useCallback
By O. Wolfson
In React, callback functions are commonly passed as props to child components, which can sometimes cause unnecessary re-renders and degrade performance. React’s useCallback
hook addresses this issue by memoizing functions and ensuring they are not recreated on each render unless necessary. This article will demonstrate the practical use of useCallback
by comparing two child components—one using useCallback
and the other not—showing the impact on re-renders.
What is a Callback Function?
A callback function is a function passed as an argument to another function, often used in React to pass event handlers or other logic from a parent component to a child.
The Problem with Callbacks in React
By default, React recreates any functions declared inside a component every time that component re-renders. If a parent component passes a callback to a child as a prop, the child will re-render when the parent re-renders, even if the callback hasn’t changed. This is problematic when the child doesn’t need to update, resulting in wasted renders and unnecessary performance costs.
Enter useCallback
The useCallback
hook helps prevent this by memoizing the function, ensuring the callback reference remains stable between renders unless its dependencies change. This is especially useful when passing callbacks to child components wrapped in React.memo()
(which skips rendering unless props change).
Demonstration: Comparing Two Child Components
Let’s create a demonstration to compare two child components—one with a callback wrapped in useCallback
and one without.
Parent Component
javascript"use client";
// ParentComponent.jsx
import React, { useState, useCallback, useRef } from "react";
import ChildWithCallback from "./ChildWithCallback";
import ChildWithoutCallback from "./ChildWithoutCallback";
function ParentComponent() {
const [count, setCount] = useState(0);
// Keep track of how many times the parent has rendered
const parentRenderCount = useRef(0);
parentRenderCount.current += 1;
// Memoized increment function
const incrementWithCallback = useCallback(() => {
setCount((c) => c + 1);
}, []);
// Regular increment function (not memoized)
const incrementWithoutCallback = () => {
setCount((c) => c + 1);
};
return (
<div className="flex flex-col gap-4">
<h1>Count: {count}</h1>
<p>Parent Render Count: {parentRenderCount.current}</p>
<h2>Child With useCallback:</h2>
<ChildWithCallback onIncrement={incrementWithCallback} />
<h2>Child Without useCallback:</h2>
<ChildWithoutCallback onIncrement={incrementWithoutCallback} />
<div>
<button onClick={() => setCount((c) => c + 1)}>
Increment from Parent
</button>
</div>
</div>
);
}
export default ParentComponent;
Child Components
Child Component Using useCallback
javascript// ChildWithCallback.jsx
import React, { useRef } from "react";
const ChildWithCallback = React.memo(({ onIncrement }) => {
const childRenderCount = useRef(0);
childRenderCount.current += 1;
return (
<div>
<p>Child With useCallback Render Count: {childRenderCount.current}</p>
<button onClick={onIncrement}>
Increment from Child With useCallback
</button>
</div>
);
});
export default ChildWithCallback;
Child Component Without useCallback
javascript// ChildWithoutCallback.jsx
import React, { useRef } from "react";
const ChildWithoutCallback = React.memo(({ onIncrement }) => {
const childRenderCount = useRef(0);
childRenderCount.current += 1;
return (
<div>
<p>Child Without useCallback Render Count: {childRenderCount.current}</p>
<button onClick={onIncrement}>
Increment from Child Without useCallback
</button>
</div>
);
});
export default ChildWithoutCallback;
Explanation
-
Parent Component:
- The parent component keeps track of its render count using
useRef
. - It passes an increment function to two child components: one that uses
useCallback
to memoize the function, and another that does not.
- The parent component keeps track of its render count using
-
Child Components:
- Both child components use
React.memo()
to prevent re-renders unless their props change. - The child with
useCallback
should only re-render when its props change, whereas the child withoutuseCallback
will re-render every time the parent does, even if the prop function remains logically the same.
- Both child components use
How to Observe the Behavior
-
Initial Load:
- Both child components will render once when the app is first loaded.
- The parent and child render counts will both start at 1.
-
Clicking "Increment from Parent":
- The parent render count will increase by 1 each time the button is clicked.
- Child With useCallback should not re-render, as the memoized
incrementWithCallback
function reference remains the same. - Child Without useCallback will re-render on every parent render because the
incrementWithoutCallback
function is recreated each time, causing the child to re-render unnecessarily.
-
Clicking "Increment from Child":
- Both child components will increment the count displayed in the parent.
- However, the re-render behavior of the child components will remain the same, demonstrating that
useCallback
prevents re-renders unless props change.
See a Deployed Demonstration
Conclusion
This demonstration shows how useCallback
can prevent unnecessary re-renders in child components by memoizing callback functions. In larger applications with many child components, this kind of optimization can significantly improve performance. By comparing two child components—one using useCallback
and one not—you can clearly see the performance benefits in preventing unnecessary renders.