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(ormemoized with a custom comparison), it will still re-render if the callback reference changes.Using callbacks in dependencies
If you pass a callback to anotheruseEffector 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,
Appre-renders. -
handleClickis recreated, soChildsees 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?
-
handleClickis memoized withuseCallback. - The reference to
handleClickremains the same between renders (unless dependencies change). -
Childwon’t re-render whencountchanges, because itsonClickprop didn’t change.
When to Use useCallback
- When passing callbacks to
memoized 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)