React useCallback
Overview
useCallback memoizes function references between renders. In 2026 with React Compiler, manual useCallback is rarely needed — the compiler auto-memoizes. Without the compiler, useCallback only helps when paired with memo() on the child component.
With React Compiler (2025+)
React Compiler 1.0 (Oct 2025) auto-applies memoization at build time. Don't add new useCallback — the compiler handles it better. If memoization is unnecessary, the compiler omits it from output.
Exceptions where manual useCallback is still needed:
- External libraries not compiled by React Compiler (older react-hook-form, animation libs)
- Functions used as
useEffectdependencies that can't be moved inside the effect - Custom hooks exporting functions that consumers demonstrably need stable — the fn lands in consumers' effect deps or memo'd children (see scope note below)
- Measured performance issues the compiler doesn't fix
Without React Compiler
useCallback only makes sense together with memo() on the child component.
// ❌ useCallback without memo = dead code
function Parent() {
const handleClick = useCallback(() => {
console.log('click');
}, []);
return <Child onClick={handleClick} />;
// Child re-renders anyway because Parent re-renders
}
// ✅ useCallback + memo = Child skips re-render
const Child = memo(function Child({ onClick }) {
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('click');
}, []);
return <Child onClick={handleClick} />;
// Child does NOT re-render — handleClick has stable reference
}
Valid Use Cases
1. useEffect dependency
const fetchData = useCallback(async () => {
const data = await fetch('/api/users');
}, []);
useEffect(() => {
fetchData();
}, [fetchData]); // stable reference = runs once
2. Custom hook exports — scope note
function useCart() {
const [items, setItems] = useState([]);
// ✅ Stable reference — consumers put addItem in effect deps
const addItem = useCallback((item) => {
setItems(prev => [...prev, item]);
}, []);
return { items, addItem };
}
This case is justified when you can NAME the consumer that needs the stability (the fn goes into a consumer's useEffect deps, a memo()'d child, or context value). For library hooks other developers consume, the doctrine is the opposite: start unmemoized, plain functions, and add useCallback only when a profiler or a concrete consumer requirement demands it — that territory belongs to react-hook-authoring, and its rules win there. This skill governs app-internal component code.
3. Context provider functions
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const logout = useCallback(() => setUser(null), []);
const value = useMemo(() => ({ user, logout }), [user, logout]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Decision
| Question | Answer |
|----------|--------|
| Using React Compiler? | Don't add useCallback manually |
| Passing fn to memo()'d child? | ✅ useCallback |
| Function in useEffect deps? | ✅ useCallback (or move fn inside effect) |
| Exporting fn from an app-internal hook, consumer needs stability? | ✅ useCallback |
| Authoring a library hook for other developers? | ❌ Start plain — react-hook-authoring governs |
| Event handler without memo on child? | ❌ Skip — it's dead code |
Common Mistake
The most over-used pattern: wrapping every event handler in useCallback "for performance" without memo on the child. The React docs say explicitly: use useCallback only for performance optimization, not as a default way to declare functions.
References
- useCallback — React docs — official API reference with usage examples and caveats
- Understanding useMemo and useCallback — Josh Comeau's deep dive on when memoization actually helps
- React Compiler — official guide on how React Compiler replaces manual useCallback
Scan to join WeChat group