Back to Blog
ReactHooksPerformanceuseCallback

React useCallback Guide

Master React useCallback hook for optimizing callback functions and preventing unnecessary re-renders.

B
Bootspring Team
Engineering
July 15, 2018
8 min read

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.

Share this article

Help spread the word about Bootspring