Back to Blog
ReactHooksuseCallbackPerformance

React useCallback Deep Dive

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

B
Bootspring Team
Engineering
November 20, 2018
6 min read

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 values

Common 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 memoized

Performance 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 helps

Best 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.

Share this article

Help spread the word about Bootspring