The useCallback hook memoizes functions to prevent unnecessary re-renders. Here's how to use it effectively.
Basic useCallback#
1import { useCallback, useState } from 'react';
2
3function Counter() {
4 const [count, setCount] = useState(0);
5
6 // Memoized callback - same reference between renders
7 const increment = useCallback(() => {
8 setCount(c => c + 1);
9 }, []);
10
11 // Without useCallback - new function every render
12 const decrement = () => {
13 setCount(c => c - 1);
14 };
15
16 return (
17 <div>
18 <p>{count}</p>
19 <Button onClick={increment}>+</Button>
20 <Button onClick={decrement}>-</Button>
21 </div>
22 );
23}Why useCallback Matters#
1import { memo, useCallback, useState } from 'react';
2
3// Memoized child component
4const ExpensiveList = memo(function ExpensiveList({ onItemClick }) {
5 console.log('ExpensiveList rendered');
6 return (
7 <ul>
8 {items.map(item => (
9 <li key={item.id} onClick={() => onItemClick(item.id)}>
10 {item.name}
11 </li>
12 ))}
13 </ul>
14 );
15});
16
17function Parent() {
18 const [count, setCount] = useState(0);
19
20 // Without useCallback: ExpensiveList re-renders on every count change
21 const handleItemClick = (id) => {
22 console.log('Clicked:', id);
23 };
24
25 // With useCallback: ExpensiveList won't re-render
26 const handleItemClickMemoized = useCallback((id) => {
27 console.log('Clicked:', id);
28 }, []);
29
30 return (
31 <div>
32 <button onClick={() => setCount(c => c + 1)}>
33 Count: {count}
34 </button>
35 <ExpensiveList onItemClick={handleItemClickMemoized} />
36 </div>
37 );
38}Dependencies Array#
1function SearchComponent({ userId }) {
2 const [query, setQuery] = useState('');
3
4 // Re-creates when userId changes
5 const search = useCallback(async () => {
6 const results = await api.search(query, userId);
7 return results;
8 }, [query, userId]);
9
10 // Empty deps - never re-creates (be careful!)
11 const resetSearch = useCallback(() => {
12 setQuery('');
13 }, []);
14
15 // Uses functional update - no query dependency needed
16 const appendToQuery = useCallback((text) => {
17 setQuery(prev => prev + text);
18 }, []);
19
20 return (/* ... */);
21}useCallback with useEffect#
1function DataFetcher({ userId }) {
2 const [data, setData] = useState(null);
3
4 // Memoized fetch function
5 const fetchData = useCallback(async () => {
6 const response = await fetch(`/api/users/${userId}`);
7 const json = await response.json();
8 setData(json);
9 }, [userId]);
10
11 // Effect depends on memoized function
12 useEffect(() => {
13 fetchData();
14 }, [fetchData]);
15
16 // Without useCallback, this would infinite loop
17 // because fetchData would be new every render
18
19 return <div>{data?.name}</div>;
20}Passing to Custom Hooks#
1function useDebounce(callback, delay) {
2 const callbackRef = useRef(callback);
3
4 // Update ref when callback changes
5 useEffect(() => {
6 callbackRef.current = callback;
7 }, [callback]);
8
9 return useCallback((...args) => {
10 const handler = setTimeout(() => {
11 callbackRef.current(...args);
12 }, delay);
13
14 return () => clearTimeout(handler);
15 }, [delay]);
16}
17
18// Usage
19function Search() {
20 const [query, setQuery] = useState('');
21
22 const search = useCallback((term) => {
23 console.log('Searching:', term);
24 }, []);
25
26 const debouncedSearch = useDebounce(search, 300);
27
28 return (
29 <input
30 value={query}
31 onChange={(e) => {
32 setQuery(e.target.value);
33 debouncedSearch(e.target.value);
34 }}
35 />
36 );
37}Event Handlers Pattern#
1function TodoList({ todos, onToggle, onDelete }) {
2 // Create stable handlers for each todo
3 const createToggleHandler = useCallback(
4 (id) => () => onToggle(id),
5 [onToggle]
6 );
7
8 const createDeleteHandler = useCallback(
9 (id) => () => onDelete(id),
10 [onDelete]
11 );
12
13 return (
14 <ul>
15 {todos.map(todo => (
16 <TodoItem
17 key={todo.id}
18 todo={todo}
19 onToggle={createToggleHandler(todo.id)}
20 onDelete={createDeleteHandler(todo.id)}
21 />
22 ))}
23 </ul>
24 );
25}
26
27// Better: Pass id to child, let child call handler
28const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
29 return (
30 <li>
31 <span onClick={() => onToggle(todo.id)}>{todo.text}</span>
32 <button onClick={() => onDelete(todo.id)}>Delete</button>
33 </li>
34 );
35});
36
37function TodoList({ todos, onToggle, onDelete }) {
38 // Stable callbacks - no need to create per-item handlers
39 const handleToggle = useCallback((id) => {
40 onToggle(id);
41 }, [onToggle]);
42
43 const handleDelete = useCallback((id) => {
44 onDelete(id);
45 }, [onDelete]);
46
47 return (
48 <ul>
49 {todos.map(todo => (
50 <TodoItem
51 key={todo.id}
52 todo={todo}
53 onToggle={handleToggle}
54 onDelete={handleDelete}
55 />
56 ))}
57 </ul>
58 );
59}useCallback vs useMemo#
1// useCallback memoizes functions
2const memoizedFn = useCallback(() => {
3 doSomething(a, b);
4}, [a, b]);
5
6// useMemo memoizes values
7const memoizedValue = useMemo(() => {
8 return computeExpensiveValue(a, b);
9}, [a, b]);
10
11// These are equivalent:
12const fn1 = useCallback(fn, deps);
13const fn2 = useMemo(() => fn, deps);
14
15// Use useCallback for functions passed as props
16// Use useMemo for computed valuesCommon Mistakes#
1// Mistake 1: Missing dependencies
2function Component({ userId }) {
3 // BUG: userId not in deps, uses stale value
4 const fetchUser = useCallback(async () => {
5 return await api.getUser(userId);
6 }, []); // Should be [userId]
7}
8
9// Mistake 2: Overusing useCallback
10function Component() {
11 // Unnecessary - not passed to child or used in deps
12 const handleClick = useCallback(() => {
13 console.log('clicked');
14 }, []);
15
16 // Just use regular function
17 const handleClick = () => {
18 console.log('clicked');
19 };
20
21 return <button onClick={handleClick}>Click</button>;
22}
23
24// Mistake 3: Creating new objects in deps
25function Component({ config }) {
26 // Re-creates every render because { ...config } is new
27 const doSomething = useCallback(() => {
28 process({ ...config });
29 }, [{ ...config }]); // Wrong!
30
31 // Fix: use config directly
32 const doSomething = useCallback(() => {
33 process({ ...config });
34 }, [config]);
35}When to Use useCallback#
1// USE when:
2
3// 1. Passing callbacks to memoized children
4const MemoChild = memo(Child);
5const handler = useCallback(() => {}, []);
6<MemoChild onClick={handler} />
7
8// 2. Callback is a dependency of useEffect/useMemo
9const fetch = useCallback(() => api.get(id), [id]);
10useEffect(() => { fetch(); }, [fetch]);
11
12// 3. Callback used in context value
13const value = useMemo(() => ({
14 data,
15 update: updateCallback,
16}), [data, updateCallback]);
17
18// DON'T USE when:
19
20// 1. Simple event handlers not passed to children
21<button onClick={() => setCount(c => c + 1)}>+</button>
22
23// 2. Component doesn't re-render often anyway
24
25// 3. Child components aren't memoizedPerformance Measurement#
1import { useCallback, memo, Profiler } from 'react';
2
3function onRenderCallback(
4 id,
5 phase,
6 actualDuration,
7 baseDuration,
8 startTime,
9 commitTime
10) {
11 console.log(`${id} ${phase}: ${actualDuration}ms`);
12}
13
14function App() {
15 return (
16 <Profiler id="App" onRender={onRenderCallback}>
17 <MyComponent />
18 </Profiler>
19 );
20}
21
22// Compare with and without useCallback
23// to see if optimization helpsBest Practices#
When to Use:
✓ Callbacks passed to memo children
✓ Callbacks in effect dependencies
✓ Callbacks in context values
✓ Expensive callback creation
Dependencies:
✓ Include all used values
✓ Use functional updates
✓ Keep deps array minimal
✓ Use refs for stable values
Performance:
✓ Profile before optimizing
✓ Combine with memo()
✓ Consider component structure
✓ Measure actual improvement
Avoid:
✗ Using without memo children
✗ Missing dependencies
✗ Overusing everywhere
✗ Premature optimization
Conclusion#
useCallback is a performance optimization that prevents function recreation between renders. Use it when passing callbacks to memoized children, when callbacks are effect dependencies, or in context values. Always include all dependencies and measure performance to ensure the optimization provides real benefits. Don't overuse it - the overhead of memoization isn't always worth it.