L6: useCallback Introduction
Memoize callback functions to prevent unnecessary child re-renders
Learn how to memoize callback functions with useCallback to prevent unnecessary component re-renders!
What You'll Learn
- What useCallback is
- Difference between useMemo and useCallback
- Prevent child re-renders
- Optimize event handlers
- Combine with React.memo
The Problem: New Functions Every Render
Functions are recreated on every render:
function Parent() {
const [count, setCount] = useState(0);
// New function created every render!
const handleClick = () => {
console.log('Clicked');
};
return <Child onClick={handleClick} />;
}Why this matters:
function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
}
export default React.memo(Child);Even with React.memo, Child re-renders because onClick is a new function reference every time!
Function identity: In JavaScript, () => {} !== () => {}. Each function is a unique object, even with identical code.
What is useCallback?
useCallback memoizes a function so it keeps the same reference across renders unless dependencies change.
Syntax:
const memoizedCallback = useCallback(() => {
// Function body
}, [dependencies]);How it works:
// First render
const handleClick = useCallback(() => { ... }, [count]);
// Creates function, saves reference
// Second render (count unchanged)
const handleClick = useCallback(() => { ... }, [count]);
// Returns saved reference (same function!)
// Third render (count changed)
const handleClick = useCallback(() => { ... }, [count]);
// Creates new function because count changeduseCallback vs useMemo
Memoizes the function itself:
const handleClick = useCallback(() => {
doSomething();
}, [dep]);Returns the memoized function.
Memoizes the function's return value:
const result = useMemo(() => {
return computeExpensiveValue();
}, [dep]);Returns the memoized value.
// These are equivalent:
const memoizedCallback = useCallback(() => {
return a + b;
}, [a, b]);
const memoizedCallback = useMemo(() => {
return () => a + b;
}, [a, b]);useCallback is shorthand for memoizing functions!
Simple Example
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// New function every render
const handleIncrement = () => {
setCount(c => c + 1);
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Child onClick={handleIncrement} />
<p>Count: {count}</p>
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered'); // Renders every keystroke!
return <button onClick={onClick}>Increment</button>;
});Problem: Typing in input re-renders Child unnecessarily!
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Same function reference across renders
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Child onClick={handleIncrement} />
<p>Count: {count}</p>
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered'); // Only renders when needed!
return <button onClick={onClick}>Increment</button>;
});Solution: Typing in input doesn't re-render Child! ✨
When to Use useCallback
Use useCallback when:
When NOT to Use useCallback
Don't use it for:
// ❌ Simple inline handlers
<button onClick={useCallback(() => setCount(c => c + 1), [])}>
Increment
</button>
// ✅ Just use inline
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
// ❌ Not passed to children
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
// Not used as prop - no benefit!
// ❌ Child not memoized
const handleClick = useCallback(() => { ... }, []);
return <RegularChild onClick={handleClick} />;
// RegularChild isn't memoized, so it re-renders anyway!Real Example: Filter Callbacks
function HomePage() {
const { data: listings } = useFetch('/listings');
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// Memoize callbacks
const handleSearchChange = useCallback((value) => {
setSearch(value);
}, []);
const handleDatesChange = useCallback((newDates) => {
setDates(newDates);
}, []);
const handleGuestsChange = useCallback((value) => {
setGuests(value);
}, []);
const filteredListings = useMemo(() => {
return listings?.filter(/* ... */);
}, [listings, search, dates, guests]);
return (
<div>
<ListingFilters
search={search}
onSearchChange={handleSearchChange}
dates={dates}
onDatesChange={handleDatesChange}
guests={guests}
onGuestsChange={handleGuestsChange}
/>
<ListingList listings={filteredListings} />
</div>
);
}
// Memoize ListingFilters to prevent unnecessary re-renders
export const ListingFilters = React.memo(function ListingFilters({
search,
onSearchChange,
dates,
onDatesChange,
guests,
onGuestsChange
}) {
console.log('ListingFilters rendered');
return (
<div className="filters">
<input
value={search}
onChange={(e) => onSearchChange(e.target.value)}
/>
{/* Date picker and guest selector */}
</div>
);
});Benefits:
ListingFiltersonly re-renders when props actually change- Not when parent re-renders for other reasons
- Typing in search doesn't recreate date/guest callbacks
useCallback with Dependencies
function Component({ userId }) {
const [data, setData] = useState(null);
// Function recreated when userId changes
const fetchUser = useCallback(async () => {
const response = await api.get(`/users/${userId}`);
setData(response.data);
}, [userId]); // userId is dependency
useEffect(() => {
fetchUser();
}, [fetchUser]);
return <div>{data?.name}</div>;
}When userId changes:
useCallbackcreates new functionuseEffectdetects new function- Fetches new user data
Common Patterns
function Component() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies - uses updater function
const handleDecrement = useCallback(() => {
setCount(c => c - 1);
}, []);
const handleReset = useCallback(() => {
setCount(0);
}, []);
return (
<Controls
onIncrement={handleIncrement}
onDecrement={handleDecrement}
onReset={handleReset}
/>
);
}function Component({ listingId }) {
const [favorite, setFavorite] = useState(false);
const toggleFavorite = useCallback(async () => {
try {
if (favorite) {
await api.delete(`/favorites/${listingId}`);
} else {
await api.post(`/favorites/${listingId}`);
}
setFavorite(!favorite);
} catch (error) {
console.error(error);
}
}, [listingId, favorite]);
return (
<button onClick={toggleFavorite}>
{favorite ? '❤️' : '🤍'}
</button>
);
}function Form() {
const [formData, setFormData] = useState({
name: '',
email: ''
});
const handleFieldChange = useCallback((field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
}, []);
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
await api.post('/submit', formData);
}, [formData]); // Depends on formData
return (
<form onSubmit={handleSubmit}>
<Input
value={formData.name}
onChange={(v) => handleFieldChange('name', v)}
/>
<Input
value={formData.email}
onChange={(v) => handleFieldChange('email', v)}
/>
</form>
);
}Best Practices
Use updater functions when possible
// ✅ Good - no dependencies needed
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);
// ❌ Less optimal - needs count dependency
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]); // Recreates when count changesCombine with React.memo
// Only works well together
const Parent = () => {
const handleClick = useCallback(() => { ... }, []);
return <MemoizedChild onClick={handleClick} />;
};
const MemoizedChild = React.memo(({ onClick }) => {
// Won't re-render unless onClick reference changes
return <button onClick={onClick}>Click</button>;
});Include all dependencies
// ✅ Correct
const handleSearch = useCallback((term) => {
const results = data.filter(item =>
item.name.includes(term)
);
setResults(results);
}, [data]); // data is dependency
// ❌ Wrong - missing data
const handleSearch = useCallback((term) => {
const results = data.filter(item =>
item.name.includes(term)
);
setResults(results);
}, []); // data changes won't be reflected!What's Next?
In Lesson 7, we'll learn about React.memo - how to prevent component re-renders by memoizing entire components. We'll combine it with useCallback for maximum optimization! 🚀
Summary
- ✅ useCallback memoizes functions
- ✅ Prevents new function references
- ✅ Use with React.memo children
- ✅ Prevents unnecessary re-renders
- ✅ Include all dependencies
- ✅ Use updater functions when possible
Key concept: useCallback = useMemo for functions. Use it to keep function references stable across renders!