September 23, 2024
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.
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.
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.
useCallbackThe 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).
Let’s create a demonstration to compare two child components—one with a callback wrapped in useCallback and one without.
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;
useCallbackjavascript// 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;
useCallbackjavascript// 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;
Parent Component:
useRef.useCallback to memoize the function, and another that does not.Child Components:
React.memo() to prevent re-renders unless their props change.useCallback should only re-render when its props change, whereas the child without useCallback will re-render every time the parent does, even if the prop function remains logically the same.Initial Load:
Clicking "Increment from Parent":
incrementWithCallback function reference remains the same.incrementWithoutCallback function is recreated each time, causing the child to re-render unnecessarily.Clicking "Increment from Child":
useCallback prevents re-renders unless props change.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.