The useCallback hook returns a memoized callback function that only changes when dependencies change, helping optimize performance.
Basic Usage#
1import { useCallback, useState } from 'react';
2
3function Counter() {
4 const [count, setCount] = useState(0);
5
6 // Without useCallback - new function every render
7 const increment = () => setCount(c => c + 1);
8
9 // With useCallback - same function reference unless deps change
10 const incrementMemoized = useCallback(() => {
11 setCount(c => c + 1);
12 }, []);
13
14 return (
15 <div>
16 <p>Count: {count}</p>
17 <button onClick={incrementMemoized}>Increment</button>
18 </div>
19 );
20}With Dependencies#
1import { useCallback, useState } from 'react';
2
3function SearchComponent() {
4 const [query, setQuery] = useState('');
5 const [results, setResults] = useState([]);
6
7 // Recreates when query changes
8 const handleSearch = useCallback(async () => {
9 const response = await fetch(`/api/search?q=${query}`);
10 const data = await response.json();
11 setResults(data);
12 }, [query]);
13
14 // Multiple dependencies
15 const [sortBy, setSortBy] = useState('name');
16 const [order, setOrder] = useState('asc');
17
18 const sortResults = useCallback((items) => {
19 return [...items].sort((a, b) => {
20 const compare = a[sortBy].localeCompare(b[sortBy]);
21 return order === 'asc' ? compare : -compare;
22 });
23 }, [sortBy, order]);
24
25 return (
26 <div>
27 <input
28 value={query}
29 onChange={e => setQuery(e.target.value)}
30 />
31 <button onClick={handleSearch}>Search</button>
32 </div>
33 );
34}Passing to Child Components#
1import { useCallback, useState, memo } from 'react';
2
3// Memoized child component
4const ExpensiveList = memo(function ExpensiveList({ items, 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 [items, setItems] = useState([
19 { id: 1, name: 'Item 1' },
20 { id: 2, name: 'Item 2' }
21 ]);
22 const [selectedId, setSelectedId] = useState(null);
23
24 // Without useCallback, ExpensiveList re-renders on every parent render
25 // because onItemClick is a new function reference
26 const handleItemClick = useCallback((id) => {
27 setSelectedId(id);
28 console.log('Item clicked:', id);
29 }, []);
30
31 return (
32 <div>
33 <ExpensiveList items={items} onItemClick={handleItemClick} />
34 <p>Selected: {selectedId}</p>
35 </div>
36 );
37}Event Handlers#
1import { useCallback, useState } from 'react';
2
3function Form() {
4 const [values, setValues] = useState({
5 name: '',
6 email: '',
7 message: ''
8 });
9
10 // Single handler for multiple inputs
11 const handleChange = useCallback((e) => {
12 const { name, value } = e.target;
13 setValues(prev => ({
14 ...prev,
15 [name]: value
16 }));
17 }, []);
18
19 const handleSubmit = useCallback((e) => {
20 e.preventDefault();
21 console.log('Submitting:', values);
22 }, [values]);
23
24 // Handler with parameters
25 const handleFieldBlur = useCallback((fieldName) => {
26 return () => {
27 console.log(`${fieldName} lost focus`);
28 };
29 }, []);
30
31 return (
32 <form onSubmit={handleSubmit}>
33 <input
34 name="name"
35 value={values.name}
36 onChange={handleChange}
37 onBlur={handleFieldBlur('name')}
38 />
39 <input
40 name="email"
41 value={values.email}
42 onChange={handleChange}
43 onBlur={handleFieldBlur('email')}
44 />
45 <textarea
46 name="message"
47 value={values.message}
48 onChange={handleChange}
49 />
50 <button type="submit">Submit</button>
51 </form>
52 );
53}With useEffect#
1import { useCallback, useEffect, useState } from 'react';
2
3function DataFetcher({ userId }) {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(false);
6
7 // Memoize fetch function
8 const fetchUser = useCallback(async () => {
9 setLoading(true);
10 try {
11 const response = await fetch(`/api/users/${userId}`);
12 const data = await response.json();
13 setUser(data);
14 } finally {
15 setLoading(false);
16 }
17 }, [userId]);
18
19 // Use in effect
20 useEffect(() => {
21 fetchUser();
22 }, [fetchUser]);
23
24 // Expose refetch capability
25 return (
26 <div>
27 {loading ? 'Loading...' : user?.name}
28 <button onClick={fetchUser}>Refresh</button>
29 </div>
30 );
31}Debounced Callbacks#
1import { useCallback, useRef, useEffect } from 'react';
2
3// Custom hook for debounced callback
4function useDebouncedCallback(callback, delay) {
5 const timeoutRef = useRef(null);
6
7 const debouncedCallback = useCallback((...args) => {
8 if (timeoutRef.current) {
9 clearTimeout(timeoutRef.current);
10 }
11
12 timeoutRef.current = setTimeout(() => {
13 callback(...args);
14 }, delay);
15 }, [callback, delay]);
16
17 // Cleanup on unmount
18 useEffect(() => {
19 return () => {
20 if (timeoutRef.current) {
21 clearTimeout(timeoutRef.current);
22 }
23 };
24 }, []);
25
26 return debouncedCallback;
27}
28
29// Usage
30function SearchInput() {
31 const [query, setQuery] = useState('');
32
33 const search = useCallback((searchTerm) => {
34 console.log('Searching for:', searchTerm);
35 // API call here
36 }, []);
37
38 const debouncedSearch = useDebouncedCallback(search, 300);
39
40 const handleChange = (e) => {
41 const value = e.target.value;
42 setQuery(value);
43 debouncedSearch(value);
44 };
45
46 return <input value={query} onChange={handleChange} />;
47}useCallback vs useMemo#
1import { useCallback, useMemo } from 'react';
2
3function Comparison() {
4 // useCallback returns memoized function
5 const memoizedCallback = useCallback(() => {
6 return 'result';
7 }, []);
8
9 // useMemo returns memoized value
10 const memoizedValue = useMemo(() => {
11 return 'result';
12 }, []);
13
14 // These are equivalent:
15 const fn1 = useCallback(someFunction, [dep]);
16 const fn2 = useMemo(() => someFunction, [dep]);
17
18 // When to use which:
19 // useCallback - for functions passed to children
20 // useMemo - for expensive calculations
21}
22
23// Practical example
24function DataProcessor({ data, onProcess }) {
25 // useMemo for computed value
26 const processedData = useMemo(() => {
27 return data.map(item => ({
28 ...item,
29 computed: expensiveComputation(item)
30 }));
31 }, [data]);
32
33 // useCallback for event handler
34 const handleClick = useCallback(() => {
35 onProcess(processedData);
36 }, [onProcess, processedData]);
37
38 return (
39 <button onClick={handleClick}>
40 Process {processedData.length} items
41 </button>
42 );
43}Avoiding Common Pitfalls#
1import { useCallback, useState } from 'react';
2
3function PitfallExamples() {
4 const [count, setCount] = useState(0);
5 const [name, setName] = useState('');
6
7 // Pitfall 1: Missing dependencies
8 // BAD - stale count value
9 const badIncrement = useCallback(() => {
10 setCount(count + 1); // Uses stale count
11 }, []); // Missing count dependency
12
13 // GOOD - use functional update
14 const goodIncrement = useCallback(() => {
15 setCount(c => c + 1); // Always current
16 }, []);
17
18 // Pitfall 2: Unnecessary useCallback
19 // BAD - no benefit, component doesn't pass this to children
20 const handleLocalClick = useCallback(() => {
21 console.log('clicked');
22 }, []);
23
24 // GOOD - just use regular function
25 const handleLocalClick2 = () => {
26 console.log('clicked');
27 };
28
29 // Pitfall 3: Inline object in dependencies
30 // BAD - object recreated every render
31 const config = { threshold: 10 };
32 const badCallback = useCallback(() => {
33 return count > config.threshold;
34 }, [count, config]); // config changes every render!
35
36 // GOOD - stable reference
37 const threshold = 10;
38 const goodCallback = useCallback(() => {
39 return count > threshold;
40 }, [count, threshold]);
41
42 return <div>{count}</div>;
43}With Context#
1import { createContext, useContext, useCallback, useState } from 'react';
2
3const TodoContext = createContext();
4
5function TodoProvider({ children }) {
6 const [todos, setTodos] = useState([]);
7
8 const addTodo = useCallback((text) => {
9 setTodos(prev => [
10 ...prev,
11 { id: Date.now(), text, completed: false }
12 ]);
13 }, []);
14
15 const toggleTodo = useCallback((id) => {
16 setTodos(prev =>
17 prev.map(todo =>
18 todo.id === id
19 ? { ...todo, completed: !todo.completed }
20 : todo
21 )
22 );
23 }, []);
24
25 const deleteTodo = useCallback((id) => {
26 setTodos(prev => prev.filter(todo => todo.id !== id));
27 }, []);
28
29 const value = {
30 todos,
31 addTodo,
32 toggleTodo,
33 deleteTodo
34 };
35
36 return (
37 <TodoContext.Provider value={value}>
38 {children}
39 </TodoContext.Provider>
40 );
41}
42
43function TodoItem({ todo }) {
44 const { toggleTodo, deleteTodo } = useContext(TodoContext);
45
46 // These are stable references from context
47 return (
48 <li>
49 <input
50 type="checkbox"
51 checked={todo.completed}
52 onChange={() => toggleTodo(todo.id)}
53 />
54 {todo.text}
55 <button onClick={() => deleteTodo(todo.id)}>Delete</button>
56 </li>
57 );
58}Custom Hooks with useCallback#
1import { useCallback, useState, useEffect } from 'react';
2
3// Fetch hook with memoized functions
4function useFetch(url) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(false);
7 const [error, setError] = useState(null);
8
9 const execute = useCallback(async () => {
10 setLoading(true);
11 setError(null);
12
13 try {
14 const response = await fetch(url);
15 if (!response.ok) throw new Error('Request failed');
16 const result = await response.json();
17 setData(result);
18 return result;
19 } catch (err) {
20 setError(err);
21 throw err;
22 } finally {
23 setLoading(false);
24 }
25 }, [url]);
26
27 const reset = useCallback(() => {
28 setData(null);
29 setError(null);
30 setLoading(false);
31 }, []);
32
33 return { data, loading, error, execute, reset };
34}
35
36// Toggle hook
37function useToggle(initialValue = false) {
38 const [value, setValue] = useState(initialValue);
39
40 const toggle = useCallback(() => setValue(v => !v), []);
41 const setTrue = useCallback(() => setValue(true), []);
42 const setFalse = useCallback(() => setValue(false), []);
43
44 return { value, toggle, setTrue, setFalse };
45}
46
47// Usage
48function Component() {
49 const { data, loading, execute } = useFetch('/api/data');
50 const { value: isOpen, toggle, setFalse: close } = useToggle();
51
52 return (
53 <div>
54 <button onClick={execute}>Fetch Data</button>
55 <button onClick={toggle}>Toggle Modal</button>
56 {isOpen && <Modal onClose={close} />}
57 </div>
58 );
59}Best Practices#
When to use useCallback:
✓ Passing callbacks to memoized children
✓ Callbacks used in useEffect dependencies
✓ Expensive callbacks that shouldn't recreate
✓ Functions exposed from custom hooks
When NOT to use:
✗ Every function (adds overhead)
✗ Functions not passed to children
✗ Simple components without memo
✗ Handlers for native elements only
Dependencies:
✓ Include all used values
✓ Use functional updates to avoid deps
✓ Keep dependencies minimal
✓ Use stable references
Performance:
✓ Pair with React.memo for children
✓ Profile before optimizing
✓ Don't prematurely optimize
✓ Measure actual performance gains
Conclusion#
useCallback memoizes callback functions to maintain stable references between renders. Use it when passing callbacks to memoized child components, when callbacks are useEffect dependencies, or when building custom hooks. Pair with React.memo for optimal performance. Avoid overusing it—only add useCallback when you have a demonstrated performance need, as unnecessary memoization adds overhead without benefit.