When building React applications, especially ones that involve complex component trees or performance-sensitive rendering, unnecessary re-renders can become a real performance bottleneck. This is where the useCallback
hook comes into play.
What is useCallback
?
useCallback
is a React hook that returns a memoized version of a callback function — that is, it ensures the function reference only changes if its dependencies change.
The syntax looks like this:
const memoizedCallback = useCallback(() => {
// Your function logic here
}, [dependencies]);
Why is it important?
In React, functions are recreated every time a component re-renders. Even if the function logic hasn’t changed, its reference in memory is new. This can cause problems in scenarios like:
Passing callbacks to child components
If the child is wrapped inReact.memo
(ormemo
ized with a custom comparison), it will still re-render if the callback reference changes.Using callbacks in dependencies
If you pass a callback to anotheruseEffect
or hook, it may trigger unintended re-execution unless the reference is stable.Performance optimization
In large applications, avoiding unnecessary re-renders saves memory and improves responsiveness.
Example Without useCallback
import React, { useState } from 'react';
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click Me</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Button clicked');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<Child onClick={handleClick} />
</div>
);
}
What happens?
- Every time you click Increase Count,
App
re-renders. -
handleClick
is recreated, soChild
sees a new prop reference and re-renders — even though the logic hasn’t changed.
Example With useCallback
import React, { useState, useCallback } from 'react';
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click Me</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<Child onClick={handleClick} />
</div>
);
}
What changes?
-
handleClick
is memoized withuseCallback
. - The reference to
handleClick
remains the same between renders (unless dependencies change). -
Child
won’t re-render whencount
changes, because itsonClick
prop didn’t change.
When to Use useCallback
- When passing callbacks to
memo
ized child components. - When a callback is used inside another hook’s dependency array.
- When re-creating a function on every render is expensive.
When NOT to Use useCallback
- If the callback is not passed to children or used in a dependency-sensitive hook.
- If the function is trivial and re-creating it is cheaper than memoizing it (over-optimization can harm readability).
Top comments (0)