useCallbackis a React hook that lets you memoize a function definition. It returns a memoized version of the callback that only changes if one of its dependencies changes. This is especially important for performance optimization when passing callbacks to optimized child components such as those usingReact.memoorshouldComponentUpdate.
Why UseuseCallback?
WithoutuseCallback, a function defined inside a component is recreated on every render. Even if the function logic stays the same, a new function reference is produced, which can trigger unnecessary re-renders in child components.
Consider this scenario:
- A parent component renders a child component.
- The parent passes a callback function as a prop.
- The parent re-renders.
- A new function instance is created.
- The child sees a changed prop reference and re-renders unnecessarily, bypassing memoization.
useCallbackensures the same function instance is reused unless its dependencies change.
Syntax
const memoizedCallback = useCallback(
() => {
// Function logic here
},
[dependency1, dependency2, ...]
);
- First argument:The callback function to memoize.
- Second argument:Dependency array controlling when the function is recreated.
Example
import React, { useState, useCallback } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked!');
setCount(c => c + 1);
}, []);
return (
Count: {count}
);
}
const MyChildComponent = React.memo(({ onClick }) => {
console.log('MyChildComponent rendered');
return ;
});
export default MyComponent;
In this example:
MyChildComponentis wrapped inReact.memo.- Without
useCallback, the child would re-render on every parent render. - With
useCallback, the function reference remains stable.
Dependency Array Considerations
- Empty dependency array (
[]):Callback is created once. Beware of stale closures. - With dependencies:Callback is recreated whenever a dependency changes.
- Omitting dependencies:Leads to stale values and bugs. ESLint can help detect this.
Stale Closures
A stale closure occurs when a callback captures values at creation time and continues using outdated values after state changes.
Example (and Fix):
import React, { useState, useCallback } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Count:', count);
setCount(count + 1);
}, []);
const handleClickCorrect = useCallback(() => {
console.log('Count:', count);
setCount(c => c + 1);
}, [count]);
return (
Count: {count}
);
}
export default MyComponent;
Solutions to Stale Closures
- Functional updates:Use
setState(prev => ...)to access the latest value. - Include dependencies:Add referenced variables to the dependency array.
When to UseuseCallback
- Passing callbacks to memoized child components.
- Callbacks used inside
useEffect. - Performance-critical components.
WhenNotto UseuseCallback
- Simple components with no performance issues.
- Callbacks with frequently changing dependencies.
- Premature optimization.
In summary,useCallbackis a powerful optimization tool, but it should be used thoughtfully, balancing performance gains with code readability.