---
name: react-use-callback
description: Guides proper usage of the useCallback hook in React. Use this skill when optimizing function references, passing callbacks to memoized components, or preventing unnecessary re-renders.
---
# React: useCallback Best Practices
## Core Principle
**useCallback caches a function definition between re-renders until its dependencies change.**
Only use `useCallback` for specific performance optimizations - not by default.
## When to Use useCallback
### 1. Passing Callbacks to Memoized Children
When passing a function to a component wrapped in `memo()`:
```jsx
import { useCallback, memo } from 'react';
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
// Expensive rendering logic
return ;
});
function Parent({ productId }) {
// Without useCallback, handleClick would be a new function every render
// causing ExpensiveChild to re-render unnecessarily
const handleClick = useCallback(() => {
console.log('Clicked:', productId);
}, [productId]);
return ;
}
```
### 2. Function as Effect Dependency
When a function is used inside `useEffect`:
```jsx
function ChatRoom({ roomId }) {
const createOptions = useCallback(() => {
return { serverUrl: 'https://localhost:1234', roomId };
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
}
```
**Better alternative:** Move the function inside the effect:
```jsx
function ChatRoom({ roomId }) {
useEffect(() => {
// Function defined inside effect - no useCallback needed
function createOptions() {
return { serverUrl: 'https://localhost:1234', roomId };
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
```
### 3. Custom Hook Return Values
Always wrap functions returned from custom hooks:
```jsx
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return { navigate, goBack };
}
```
### 4. Reducing State Dependencies
Use updater functions to eliminate state dependencies:
```jsx
// Before: todos is a dependency
const handleAddTodo = useCallback((text) => {
setTodos([...todos, { id: nextId++, text }]);
}, [todos]);
// After: No todos dependency needed
const handleAddTodo = useCallback((text) => {
setTodos(todos => [...todos, { id: nextId++, text }]);
}, []);
```
## When NOT to Use useCallback
### 1. Child Is Not Memoized
Without `memo()`, `useCallback` provides no benefit:
```jsx
// useCallback is pointless here
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// Child will re-render anyway when Parent re-renders
return ;
}
```
### 2. Coarse Interactions
Apps with page-level navigation don't benefit from memoization:
```jsx
// Overkill for simple navigation
function App() {
const [page, setPage] = useState('home');
// Not needed - page transitions are inherently expensive anyway
const navigate = useCallback((page) => setPage(page), []);
return ;
}
```
### 3. When Better Alternatives Exist
**Accept JSX as children:**
```jsx
// Instead of memoizing onClick
function Panel({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
{isOpen && children}
);
}
// Children don't re-render when Panel's state changes
```
**Keep state local:**
```jsx
// Don't lift state higher than necessary
function SearchForm() {
// Local state doesn't trigger parent re-renders
const [query, setQuery] = useState('');
return setQuery(e.target.value)} />;
}
```
## Anti-Patterns to Avoid
### Missing Dependency Array
```jsx
// Returns a new function every render
const handleClick = useCallback(() => {
doSomething();
}); // Missing dependency array!
// Correct
const handleClick = useCallback(() => {
doSomething();
}, []);
```
### useCallback in Loops
```jsx
// Can't call hooks in loops
function List({ items }) {
return items.map(item => {
// WRONG
const handleClick = useCallback(() => sendReport(item), [item]);
return ;
});
}
// Correct: Extract to component
function List({ items }) {
return items.map(item => (
));
}
function Report({ item }) {
const handleClick = useCallback(() => sendReport(item), [item]);
return ;
}
// Alternative: Wrap Report in memo instead
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return ;
});
```
## useCallback vs useMemo
| Hook | Caches | Use Case |
|------|--------|----------|
| `useCallback(fn, deps)` | The function itself | Callback props |
| `useMemo(() => fn, deps)` | Result of calling function | Computed values |
```jsx
// Equivalent
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);
```
## Quick Reference
### DO
- Use with `memo()` wrapped children
- Use when function is an effect dependency
- Wrap custom hook return functions
- Use updater functions to reduce dependencies
### DON'T
- Add everywhere "just in case"
- Use without `memo()` on child component
- Use when you can restructure code instead
- Forget the dependency array
## Performance Debugging
When memoization isn't working, debug dependencies:
```jsx
const handleSubmit = useCallback((orderDetails) => {
// ...
}, [productId, referrer]);
console.log([productId, referrer]);
```
Check in browser console:
```js
Object.is(temp1[0], temp2[0]); // First dependency same?
Object.is(temp1[1], temp2[1]); // Second dependency same?
```
## Future: React Compiler
React Compiler automatically memoizes values and functions, reducing the need for manual `useCallback` calls. Consider using the compiler to handle memoization automatically.
## References
- [React Docs - useCallback](https://react.dev/reference/react/useCallback)
- [React Docs - memo](https://react.dev/reference/react/memo)
- [React Docs - useMemo](https://react.dev/reference/react/useMemo)