Why Use useCallback Hook in React
This article explains the purpose of the useCallback
hook in React, detailing how it optimizes application performance by
memoizing function definitions. You will learn how
useCallback prevents unnecessary child component
re-renders, stabilizes dependency arrays in other hooks, and when you
should (and should not) apply it in your codebase.
Understanding the Problem: Referential Equality
In React, every time a component re-renders, all functions declared inside it are recreated from scratch. This happens because JavaScript treats functions as first-class objects, meaning two functions with the exact same code are not considered equal if they point to different memory addresses (referential inequality).
// Each render creates a new instance of this function
const handleClick = () => {
console.log("Clicked");
};While recreating functions is fast and generally not a performance issue on its own, it causes problems when those functions are passed as props to optimized child components or used as dependencies in other hooks.
Key Reasons to Use useCallback
1. Preventing Unnecessary Re-renders of Child Components
When you pass a function as a prop to a child component, the child component detects a “new” prop on every render of the parent, even if the function’s logic hasn’t changed.
If the child component is wrapped in React.memo, it is
supposed to skip re-rendering if its props remain the same. However,
passing a freshly recreated function defeats React.memo
entirely. Wrapping the function in useCallback ensures that
the same function reference is passed across renders, allowing
React.memo to work successfully.
import React, { useState, useCallback } from 'react';
// Child component wrapped in React.memo
const Button = React.memo(({ handleClick }) => {
console.log("Button rendered");
return <button onClick={handleClick}>Click me</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
// useCallback keeps this function reference stable
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []); // Empty dependency array means the function reference never changes
return (
<div>
<p>Count: {count}</p>
<Button handleClick={increment} />
</div>
);
}2. Stabilizing Hook Dependency Arrays
If you use a function inside a useEffect,
useMemo, or another useCallback hook, you must
include that function in the dependency array. If the function is
recreated on every render, the dependent hook will run on every single
render, potentially causing infinite loops or performance
degradation.
Using useCallback ensures the function reference only
changes when its own dependencies change, preventing unnecessary hook
executions.
const fetchData = useCallback(() => {
// Fetch logic using "userId"
fetch(`https://api.example.com/user/${userId}`)
.then(res => res.json());
}, [userId]); // Only updates when userId changes
useEffect(() => {
fetchData();
}, [fetchData]); // Safely triggered only when fetchData reference changesWhen Not to Use useCallback
It is a common mistake to wrap every single function in
useCallback. Do not use it in the following scenarios:
- The child component is not memoized: If the
receiving component does not use
React.memo, it will re-render anyway, makinguseCallbackuseless. - Simple inline event handlers: For standard HTML
elements (like
<button onClick={handler}>), referential identity does not matter. - The cost outweighs the benefit:
useCallbackhas its own overhead. It requires memory to store the dependency array and the function definition, and it performs comparison checks on every render. Overusing it can actually slow down your application.