Think of useCallback as a way to tell React: “Remember this function, and only give me a new version if something specific changes.” By default, when a component re-renders, any functions defined inside it are created fresh. useCallback helps you avoid this recreation unless necessary.
Why is this important?
Imagine you pass a function from a parent component to a child component. If the parent re-renders and creates a new function every time, even if the function’s code is identical, the child component sees it as a different prop. This often causes the child to re-render unnecessarily, even if its own data hasn’t changed. useCallback prevents this by ensuring the same function reference is passed down unless its dependencies change.
Syntax
import { useCallback } from 'react';
const memoizedCallback = useCallback(
() => {
// Your function logic here
},
[/* dependencies */]
);
useCallbacktakes two arguments:- The function you want to memoize.
- A dependency array (
[/* dependencies */]).
How Dependencies Work
The dependency array is crucial. useCallback will only return a new version of your function if any value in this array changes between renders. If the array is empty ([]), the function is created once and never changes. If you use a value from the component (like a state variable) inside your function, it must be included in the dependencies.
Simple Examples of useCallback
Let’s look at some easy-to-understand scenarios:
Example 1: Basic useCallback to Prevent Unnecessary Child Re-renders
This is the most common use case. Let’s make the parent and child components as simple as possible.
// ParentComponent.jsx
import React, { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent'; // Assume this component exists
function ParentComponent() {
const [parentCount, setParentCount] = useState(0);
const [otherState, setOtherState] = useState(false); // Another piece of state
// This function uses useCallback
// It only changes if 'setParentCount' changes (which is unlikely)
const incrementCount = useCallback(() => {
setParentCount((prevCount) => prevCount + 1);
}, [setParentCount]); // React guarantees setter stability, so [] often works too.
const toggleOtherState = () => {
setOtherState(!otherState);
};
return (
<div>
<p>Parent Count: {parentCount}</p>
<button onClick={toggleOtherState}>
Toggle Other State ({String(otherState)})
</button>
{/* Passing the memoized function */}
<ChildComponent onIncrement={incrementCount} />
</div>
);
}
export default ParentComponent;
// ChildComponent.jsx
import React from 'react';
// React.memo is key here. It prevents the component from re-rendering
// if its props (like onIncrement) haven't changed in a shallow comparison.
const ChildComponent = React.memo(({ onIncrement }) => {
console.log("ChildComponent is rendering"); // Add this to see when it renders
return (
<div>
<p>I'm a child component</p>
<button onClick={onIncrement}>Click me to increment parent count!</button>
</div>
);
});
export default ChildComponent;
- What happens?
- Clicking the “Toggle Other State” button causes
ParentComponentto re-render (becauseotherStatechanged). - Without
useCallback, theincrementCountfunction would be created fresh during this re-render.ChildComponentwould see a newonIncrementprop and re-render, even though it doesn’t need to. - With
useCallback, theincrementCountfunction reference stays the same across parent re-renders (as long as its dependencies don’t change).React.memosees that theonIncrementprop hasn’t changed and skips re-renderingChildComponent. You’ll only see the console log when the component mounts or genuinely needs to update.
- Clicking the “Toggle Other State” button causes
Example 2: useCallback with Dependencies
What if your function needs to change based on some value?
// GreeterComponent.jsx
import React, { useState, useCallback } from 'react';
function GreeterComponent() {
const [greeting, setGreeting] = useState('Hello');
const [name, setName] = useState('World');
// This function depends on the 'name' state
// It will be recreated only if 'name' changes
const greet = useCallback(() => {
alert(`${greeting}, ${name}!`); // Uses both greeting and name
}, [name, greeting]); // Include BOTH 'name' AND 'greeting' in dependencies
// Omitting 'greeting' here would be a bug if 'greeting' could change elsewhere
return (
<div>
<input
type="text"
value={greeting}
onChange={(e) => setGreeting(e.target.value)}
placeholder="Greeting"
/>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<button onClick={greet}>Show Greeting</button>
</div>
);
}
export default GreeterComponent;
// If you were passing 'greet' to a child component wrapped in React.memo,
// that child would only re-render if 'name' or 'greeting' changed.
When Should You Use useCallback?
- Passing callbacks to optimized children: This is the primary use case. Use it when you pass a function to a child component wrapped in
React.memoto prevent unnecessary re-renders of that child. - Stable dependencies for other hooks: Sometimes, you might have a custom hook or another hook (like
useEffect) that depends on a function. UsinguseCallbackensures the function reference is stable, preventing the dependent hook from running unnecessarily.
Common Pitfalls and Best Practices
- Don’t overuse it: If you’re not passing the function to a
React.memocomponent or using it as a stable dependency for another hook,useCallbackmight be unnecessary and add slight overhead. - Dependency array is crucial: Always include all values from the component scope (props, state, other variables) that your memoized function reads. Missing dependencies can lead to stale closures and bugs. Tools like the
exhaustive-depsESLint rule can help catch these. - Combine with
React.memo:useCallbackis most effective when used withReact.memoon the receiving component.React.memochecks if props have changed before deciding to re-render.
FAQs
-
What’s the difference between
useCallbackanduseMemo?useCallback(fn, deps)is essentially shorthand foruseMemo(() => fn, deps).useCallbackmemoizes the function itself.useMemomemoizes the result of running a function. UseuseMemofor expensive calculations whose results you want to cache.
-
Can I use
useCallbackin class components?- No. Hooks like
useCallbackcan only be used inside functional components. Class components have different patterns likePureComponentorshouldComponentUpdatefor optimizations.
- No. Hooks like
-
Does
useCallbackalways improve performance?- Not always. There’s a small cost to using
useCallback. It’s most beneficial when it prevents re-renders of expensive child components or stabilizes dependencies for other hooks. Profile your app to see if it helps.
- Not always. There’s a small cost to using
Conclusion
useCallback is a valuable React hook for optimizing performance by memoizing functions. It shines when preventing unnecessary re-renders of child components via React.memo. Remember to use it judiciously, always include the correct dependencies, and pair it with React.memo for maximum effect. Understanding useCallback helps you build more efficient React applications.
Reading Further
- React Design Patterns
- useTransition Hook in React
- useDeferredValue Hook in React
- useSyncExternalStore Hook in React