When to Use `useCallback` in React
Many people know that use useCallback
in React can optimize performance by memoizing functions. However, not every function needs to be wrapped in useCallback
. Overusing useCallback
can add unnecessary overhead to your project.
useCallback
Not Every Function Needs When should you use useCallback
? This is a common question among developers.
Here are three frequently asked questions:
- Why not wrap every function in
useCallback
? - Isn’t
useCallback
a caching tool? - Wouldn’t caching every function improve performance?
It’s true that useCallback
is a caching tool. However, it does not prevent the recreation of functions.
Example
// Example Component
const ExampleComponent = () => {
// fun 1: Function wrapped in useCallback
const fun1 = useCallback(() => {
console.log("Example function 1");
...
}, []);
// fun 2: Function not wrapped in useCallback
const fun2 = () => {
console.log("Example function 2");
...
};
return <div></div>;
};
In this ExampleComponent
component, there are two functions: fun1
(wrapped in useCallback
) and fun2
(not wrapped in useCallback
).
At first glance, you might think that when the ExampleComponent
component re-renders, only fun2
will be recreated while fun1
remains the same. However, this is not true. Functions wrapped in useCallback
are also recreated and passed as arguments to useCallback
.
The primary purpose of useCallback
is not to prevent functions from being recreated but to return the same function reference (old function address) when dependencies remain unchanged. Regardless of whether useCallback
is used, functions will still be recreated when a component renders.
useCallback
Hidden Cost of Overusing Every function wrapped in useCallback
is managed by useCallback
's internal queue. If you overuse useCallback
, the queue grows, and every re-render of a component requires useCallback
to:
- Traverse its internal queue to find the relevant functions.
- Check if the dependencies of those functions have changed.
Both actions consume resources. Overusing useCallback
doesn’t prevent function recreation but adds unnecessary overhead to your project.
useCallback
When to Use The correct scenario for using useCallback
is when you pass a function to a child component that is memoized using React.memo
.
useCallback
ensures that the function reference remains unchanged when dependencies do not change, which prevents unnecessary re-renders of memoized child components.
How React.memo Works
React.memo
is a caching tool that prevents a component from re-rendering if its props haven’t changed. Specifically, React.memo
compares the memory addresses of the props to detect changes.
When you pass a function as a prop, React detects changes if the function reference changes. If the parent component re-renders (even for state changes unrelated to the child component), all functions in the parent are recreated, and their references change. This triggers a re-render of the memoized child component.
useCallback
Example 1: Without import { memo } from "react";
/** Parent Component **/
const Parent = () => {
const [parentState, setParentState] = useState(0); // Parent state
// Function to be passed to child component
const toChildFun = () => {
console.log("Function passed to child component");
// Additional logic here
};
return (
<div>
<button onClick={() => setParentState((val) => val + 1)}>
Click to change parent state
</button>
{/* Pass function to child */}
<Child fun={toChildFun}></Child>
</div>
);
};
/** Memoized Child Component **/
const Child = memo(() => {
console.log("Child component re-rendered");
return <div></div>;
});
Behavior:
When you click the button in the parent component, the parent re-renders because its state (parentState
) changes. Since the toChildFun
function reference changes, the memoized child component detects the change in its props and re-renders.
useCallback
Example 2: With import { useCallback, memo } from "react";
/** Parent Component **/
const Parent = () => {
const [parentState, setParentState] = useState(0); // Parent state
// Function to be passed to child component (wrapped in useCallback)
const toChildFun = useCallback(() => {
console.log("Function passed to child component");
// Additional logic here
}, []);
return (
<div>
<button onClick={() => setParentState((val) => val + 1)}>
Click to change parent state
</button>
{/* Pass function to child */}
<Child fun={toChildFun}></Child>
</div>
);
};
/** Memoized Child Component **/
const Child = memo(() => {
console.log("Child component re-rendered");
return <div></div>;
});
Behavior:
The parent component re-renders when its state changes, but the toChildFun
function reference remains unchanged because of useCallback
. The memoized child component does not detect a change in its props and does not re-render.
Summary
- Don’t wrap every function in
useCallback
. OverusinguseCallback
can introduce performance overhead rather than optimization. useCallback
doesn’t prevent function recreation. It ensures that the same function reference is returned when dependencies don’t change.- Use
useCallback
withReact.memo
. It’s particularly useful when passing functions as props to memoized child components to prevent unnecessary re-renders.