The useCallback hook memoizes callback functions to prevent unnecessary re-renders. Here's when and how to use it effectively.
Basic Usage#
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 // New function every render - NO useCallback
12 const decrement = () => {
13 setCount((c) => c - 1);
14 };
15
16 return (
17 <div>
18 <p>Count: {count}</p>
19 <button onClick={increment}>+</button>
20 <button onClick={decrement}>-</button>
21 </div>
22 );
23}With Dependencies#
1import { useCallback, useState } from 'react';
2
3function SearchComponent() {
4 const [query, setQuery] = useState('');
5 const [results, setResults] = useState([]);
6
7 // Callback depends on query
8 const search = useCallback(async () => {
9 const data = await fetch(`/api/search?q=${query}`);
10 setResults(await data.json());
11 }, [query]); // Re-create when query changes
12
13 // Callback with parameter
14 const handleSelect = useCallback((id: string) => {
15 console.log('Selected:', id);
16 }, []); // No dependencies - stable reference
17
18 return (
19 <div>
20 <input
21 value={query}
22 onChange={(e) => setQuery(e.target.value)}
23 />
24 <button onClick={search}>Search</button>
25 <ResultList results={results} onSelect={handleSelect} />
26 </div>
27 );
28}With React.memo#
1import { memo, useCallback, useState } from 'react';
2
3// Memoized child component
4const ExpensiveList = memo(function ExpensiveList({
5 items,
6 onItemClick,
7}: {
8 items: string[];
9 onItemClick: (item: string) => void;
10}) {
11 console.log('ExpensiveList rendered');
12 return (
13 <ul>
14 {items.map((item) => (
15 <li key={item} onClick={() => onItemClick(item)}>
16 {item}
17 </li>
18 ))}
19 </ul>
20 );
21});
22
23function Parent() {
24 const [count, setCount] = useState(0);
25 const [items] = useState(['a', 'b', 'c']);
26
27 // Without useCallback, ExpensiveList re-renders on every count change
28 // because onItemClick would be a new function reference
29 const handleItemClick = useCallback((item: string) => {
30 console.log('Clicked:', item);
31 }, []);
32
33 return (
34 <div>
35 <button onClick={() => setCount((c) => c + 1)}>
36 Count: {count}
37 </button>
38 <ExpensiveList items={items} onItemClick={handleItemClick} />
39 </div>
40 );
41}Event Handlers#
1import { useCallback, useState } from 'react';
2
3function Form() {
4 const [form, setForm] = useState({ name: '', email: '' });
5
6 // Generic change handler
7 const handleChange = useCallback(
8 (field: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) => {
9 setForm((prev) => ({ ...prev, [field]: e.target.value }));
10 },
11 []
12 );
13
14 // Specific handlers (memoized)
15 const handleNameChange = useCallback(
16 (e: React.ChangeEvent<HTMLInputElement>) => {
17 setForm((prev) => ({ ...prev, name: e.target.value }));
18 },
19 []
20 );
21
22 const handleEmailChange = useCallback(
23 (e: React.ChangeEvent<HTMLInputElement>) => {
24 setForm((prev) => ({ ...prev, email: e.target.value }));
25 },
26 []
27 );
28
29 return (
30 <form>
31 <input value={form.name} onChange={handleNameChange} />
32 <input value={form.email} onChange={handleEmailChange} />
33 {/* Or using generic handler */}
34 <input value={form.name} onChange={handleChange('name')} />
35 </form>
36 );
37}With useEffect#
1import { useCallback, useEffect, useState } from 'react';
2
3function DataFetcher({ userId }: { userId: string }) {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(false);
6
7 // Memoized fetch function
8 const fetchUser = useCallback(async () => {
9 setLoading(true);
10 try {
11 const response = await fetch(`/api/users/${userId}`);
12 setUser(await response.json());
13 } finally {
14 setLoading(false);
15 }
16 }, [userId]);
17
18 // Effect depends on memoized callback
19 useEffect(() => {
20 fetchUser();
21 }, [fetchUser]);
22
23 // Can also be called manually
24 const handleRefresh = () => {
25 fetchUser();
26 };
27
28 return (
29 <div>
30 {loading ? 'Loading...' : user?.name}
31 <button onClick={handleRefresh}>Refresh</button>
32 </div>
33 );
34}Custom Hooks#
1import { useCallback, useState } from 'react';
2
3// Custom hook with memoized callbacks
4function useCounter(initialValue = 0) {
5 const [count, setCount] = useState(initialValue);
6
7 const increment = useCallback(() => {
8 setCount((c) => c + 1);
9 }, []);
10
11 const decrement = useCallback(() => {
12 setCount((c) => c - 1);
13 }, []);
14
15 const reset = useCallback(() => {
16 setCount(initialValue);
17 }, [initialValue]);
18
19 const setValue = useCallback((value: number) => {
20 setCount(value);
21 }, []);
22
23 return { count, increment, decrement, reset, setValue };
24}
25
26// Custom hook for toggle
27function useToggle(initialValue = false) {
28 const [value, setValue] = useState(initialValue);
29
30 const toggle = useCallback(() => {
31 setValue((v) => !v);
32 }, []);
33
34 const setTrue = useCallback(() => {
35 setValue(true);
36 }, []);
37
38 const setFalse = useCallback(() => {
39 setValue(false);
40 }, []);
41
42 return { value, toggle, setTrue, setFalse };
43}Context Optimization#
1import { createContext, useCallback, useContext, useMemo, useState } from 'react';
2
3interface AuthContextValue {
4 user: User | null;
5 login: (credentials: Credentials) => Promise<void>;
6 logout: () => void;
7}
8
9const AuthContext = createContext<AuthContextValue | null>(null);
10
11function AuthProvider({ children }: { children: React.ReactNode }) {
12 const [user, setUser] = useState<User | null>(null);
13
14 // Memoized callbacks prevent context consumers from re-rendering
15 const login = useCallback(async (credentials: Credentials) => {
16 const response = await fetch('/api/login', {
17 method: 'POST',
18 body: JSON.stringify(credentials),
19 });
20 setUser(await response.json());
21 }, []);
22
23 const logout = useCallback(() => {
24 setUser(null);
25 }, []);
26
27 // Memoize context value
28 const value = useMemo(
29 () => ({ user, login, logout }),
30 [user, login, logout]
31 );
32
33 return (
34 <AuthContext.Provider value={value}>
35 {children}
36 </AuthContext.Provider>
37 );
38}Debounced Callbacks#
1import { useCallback, useRef, useEffect } from 'react';
2
3function useDebounce<T extends (...args: any[]) => any>(
4 callback: T,
5 delay: number
6): T {
7 const timeoutRef = useRef<NodeJS.Timeout>();
8
9 useEffect(() => {
10 return () => {
11 if (timeoutRef.current) {
12 clearTimeout(timeoutRef.current);
13 }
14 };
15 }, []);
16
17 return useCallback(
18 ((...args) => {
19 if (timeoutRef.current) {
20 clearTimeout(timeoutRef.current);
21 }
22 timeoutRef.current = setTimeout(() => {
23 callback(...args);
24 }, delay);
25 }) as T,
26 [callback, delay]
27 );
28}
29
30// Usage
31function SearchInput() {
32 const [query, setQuery] = useState('');
33
34 const search = useCallback((term: string) => {
35 console.log('Searching for:', term);
36 // API call here
37 }, []);
38
39 const debouncedSearch = useDebounce(search, 300);
40
41 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
42 const value = e.target.value;
43 setQuery(value);
44 debouncedSearch(value);
45 };
46
47 return <input value={query} onChange={handleChange} />;
48}List Item Callbacks#
1import { memo, useCallback } from 'react';
2
3interface Item {
4 id: string;
5 name: string;
6}
7
8const ListItem = memo(function ListItem({
9 item,
10 onDelete,
11 onEdit,
12}: {
13 item: Item;
14 onDelete: (id: string) => void;
15 onEdit: (id: string) => void;
16}) {
17 return (
18 <li>
19 {item.name}
20 <button onClick={() => onEdit(item.id)}>Edit</button>
21 <button onClick={() => onDelete(item.id)}>Delete</button>
22 </li>
23 );
24});
25
26function ItemList({ items }: { items: Item[] }) {
27 // Stable callbacks for all items
28 const handleDelete = useCallback((id: string) => {
29 console.log('Delete:', id);
30 }, []);
31
32 const handleEdit = useCallback((id: string) => {
33 console.log('Edit:', id);
34 }, []);
35
36 return (
37 <ul>
38 {items.map((item) => (
39 <ListItem
40 key={item.id}
41 item={item}
42 onDelete={handleDelete}
43 onEdit={handleEdit}
44 />
45 ))}
46 </ul>
47 );
48}Ref Callbacks#
1import { useCallback, useState } from 'react';
2
3function MeasuredComponent() {
4 const [height, setHeight] = useState(0);
5
6 // Callback ref - called when element mounts/unmounts
7 const measureRef = useCallback((node: HTMLDivElement | null) => {
8 if (node) {
9 setHeight(node.getBoundingClientRect().height);
10 }
11 }, []);
12
13 return (
14 <div ref={measureRef}>
15 <p>This element's height is: {height}px</p>
16 <p>Content that determines height...</p>
17 </div>
18 );
19}
20
21// With ResizeObserver
22function ResizeAwareComponent() {
23 const [size, setSize] = useState({ width: 0, height: 0 });
24
25 const observerRef = useCallback((node: HTMLDivElement | null) => {
26 if (!node) return;
27
28 const observer = new ResizeObserver(([entry]) => {
29 setSize({
30 width: entry.contentRect.width,
31 height: entry.contentRect.height,
32 });
33 });
34
35 observer.observe(node);
36
37 return () => observer.disconnect();
38 }, []);
39
40 return (
41 <div ref={observerRef}>
42 Size: {size.width} x {size.height}
43 </div>
44 );
45}When NOT to Use useCallback#
1// ❌ Don't use for simple inline handlers without memo children
2function SimpleComponent() {
3 const [count, setCount] = useState(0);
4
5 // This is fine - no need for useCallback
6 return (
7 <button onClick={() => setCount((c) => c + 1)}>
8 Count: {count}
9 </button>
10 );
11}
12
13// ❌ Don't use when the component is cheap to render
14function CheapComponent({ onClick }: { onClick: () => void }) {
15 return <button onClick={onClick}>Click</button>;
16}
17
18// ❌ Don't use when dependencies change every render
19function BadExample({ data }: { data: object }) {
20 // This defeats the purpose - new array every render
21 const items = [data];
22
23 const handleClick = useCallback(() => {
24 console.log(items);
25 }, [items]); // items is new every render!
26}Best Practices#
When to Use:
✓ Passing callbacks to memoized children
✓ Callbacks in useEffect dependencies
✓ Callbacks in custom hooks
✓ Context provider values
When to Skip:
✓ Simple inline handlers
✓ Components without memo
✓ Dependencies that change often
✓ Very cheap operations
Patterns:
✓ Combine with React.memo
✓ Use functional updates
✓ Minimize dependencies
✓ Consider useReducer for complex state
Avoid:
✗ Premature optimization
✗ Every function needs useCallback
✗ Unstable dependencies
✗ Over-memoization
Conclusion#
useCallback memoizes callback functions to maintain stable references across renders. Use it when passing callbacks to memoized child components (React.memo), when callbacks are dependencies in useEffect, or in custom hooks that return functions. Avoid overusing it for simple cases where re-renders are cheap. The key is measuring performance and applying memoization where it provides measurable benefits.