Back to Blog
ReactHooksuseCallbackPerformance

React useCallback Optimization Guide

Master React useCallback for optimizing component performance by memoizing callback functions.

B
Bootspring Team
Engineering
August 27, 2019
7 min read

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.

Share this article

Help spread the word about Bootspring