logo
Back
Share
Share to LinkedIn
Share to Facebook
Share to Twitter
Share to Hacker News
Share to Telegram
Table of Contents
FrontendReactNextjs

When to Use useCallback in React: A Complete Guide with Real Examples

October 3, 2025
2 min read
69 views
When to Use useCallback in React: A Complete Guide with Real Examples

Learn when and why to use React's useCallback hook to optimize your application's performance. This comprehensive guide covers practical use cases, real-world examples, common pitfalls, and best practices to help you make informed decisions about memoizing functions in your React components.

Introduction

The useCallback hook is one of React's performance optimization tools that helps prevent unnecessary re-renders by memoizing function references between renders. However, it's often misused or overused, leading to more complexity without real performance gains. This guide will help you understand exactly when useCallback is beneficial and when it's unnecessary.

What is useCallback?

useCallback is a React Hook that returns a memoized version of a callback function that only changes if one of its dependencies has changed. It caches a function definition between re-renders until its dependencies change.

Syntax:

Code
1const memoizedCallback = useCallback(() => { 2 // Your function logic 3}, [dependencies] // Dependency array );

When You SHOULD Use useCallback

1. Passing Functions to Memoized Child Components

If a function is only used within the component and not passed anywhere, memoizing adds unnecessary complexity.

Code
1// DON'T do this 2function Counter() { 3 const [count, setCount] = useState(0); 4 const increment = useCallback(() => { 5 setCount(c => c + 1); 6 }, []); // Unnecessary! 7 return <button onClick={increment}>Count: {count}</button>; 8} 9 10// DO this instead 11function Counter() { 12 const [count, setCount] = useState(0); 13 const increment = () => { 14 setCount(c => c + 1); 15 }; 16 return <button onClick={increment}>Count: {count}</button>; 17}

2. Functions Without Dependencies

If your callback has no dependencies or the component rarely re-renders, useCallback adds unnecessary overhead without providing any benefit.

3. Every Function in Your Component

Overusing useCallback on every function makes code harder to read and maintain without real performance gains.

Benefits of useCallback

Performance optimization by preventing unnecessary re-renders
Selective rendering of child components
Preventing memory leaks by avoiding function recreation
Stable function references for event listeners and effects

Drawbacks and Trade-offs

Increased code complexity that can be harder to understand
Memory overhead from memoization itself
Potential bugs if dependency array is incorrect
Premature optimization when performance issues don't exist

Best Practices

Only use when needed: Measure performance first. Don't use useCallback everywhere "just in case".
Use functional updates: When updating state based on previous value, use functional updates to avoid adding state to dependencies:
Code
1// Good 2const increment = useCallback(() => { 3 setCount(prev => prev + 1); 4}, []); // No dependencies needed 5 6// Less optimal 7const increment = useCallback(() => { 8 setCount(count + 1); 9}, [count]); // Recreates on every count change
Keep dependency arrays accurate: Always include all variables used inside the callback.
Pair with React.memo: useCallback is most effective when used with React.memo on child components.
Profile before optimizing: Use React DevTools Profiler to identify actual performance bottlenecks before adding memoization

Real-World Example: Form with Multiple Fields:

Code
1function UserForm() { 2 const [formData, setFormData] = useState({ name: '', email: '', bio: '' }); 3 4 // Memoize handlers to prevent Input re-renders 5 const handleNameChange = useCallback((e) => { 6 setFormData(prev => ({ ...prev, name: e.target.value })); 7 }, []); 8 9 const handleEmailChange = useCallback((e) => { 10 setFormData(prev => ({ ...prev, email: e.target.value })); 11 }, []); 12 13 const handleBioChange = useCallback((e) => { 14 setFormData(prev => ({ ...prev, bio: e.target.value })); 15 }, []); 16 17 return ( 18 <form> 19 <MemoizedInput 20 value={formData.name} 21 onChange={handleNameChange} 22 placeholder="Name" 23 /> 24 25 <MemoizedInput 26 value={formData.email} 27 onChange={handleEmailChange} 28 placeholder="Email" 29 /> 30 31 <MemoizedTextarea 32 value={formData.bio} 33 onChange={handleBioChange} 34 placeholder="Bio" 35 /> 36 </form> 37 ); 38} 39 40const MemoizedInput = React.memo(({ value, onChange, placeholder }) => { 41 console.log(`Input ${placeholder} rendered`); 42 43 return 44 <input 45 value={value} 46 onChange={onChange} 47 placeholder={placeholder} 48 />; 49});

Conclusion

Use useCallback when you genuinely need referential equality for functions—primarily when passing them to memoized child components or using them as dependencies in other hooks. Don't use it everywhere by default, as premature optimization can make code harder to maintain without delivering real performance benefits.

Remember: Profile first, optimize second. React is already quite fast, and most applications won't benefit from aggressive memoization. Use useCallback strategically, where it actually makes a measurable difference.

Tags

FrontendReactNextjs