Self-paced

Explore our extensive collection of courses designed to help you master various subjects and skills. Whether you're a beginner or an advanced learner, there's something here for everyone.

Bootcamp

Learn live

Join us for our free workshops, webinars, and other events to learn more about our programs and get started on your journey to becoming a developer.

Upcoming live events

Learning library

For all the self-taught geeks out there, here is our content library with most of the learning materials we have produced throughout the years.

It makes sense to start learning by reading and watching videos about fundamentals and how things work.

Search from all Lessons


← Back to Lessons

The useCallback Hook in React: Memoizing Functions for Performance

What is useCallback?
useCallback vs. Regular Functions

In React applications, function components re-render when their props or state change. During each render, the functions defined inside your component are recreated. This is usually fine, but can cause performance issues in certain scenarios. The useCallback hook helps solve this problem by memoizing callback functions.

What is useCallback?

useCallback is a React hook that returns a memoized version of the callback function you provide. This memoized function will only change if one of its dependencies has changed. This can help prevent unnecessary re-renders of components that rely on reference equality of functions.

1const memoizedCallback = useCallback( 2 () => { 3 doSomething(a, b); 4 }, 5 [a, b], 6);

In this syntax:

  • The first argument is the function you want to memoize
  • The second argument is an array of dependencies (similar to useEffect and useMemo)
  • The hook returns a memoized version of your function that only changes when dependencies change

Why Do We Need useCallback?

In React, when a component re-renders, everything inside that component gets recreated, including functions. This means:

1function ParentComponent() { 2 // This function is recreated on every render 3 const handleClick = () => { 4 console.log('Clicked!'); 5 }; 6 7 return <ChildComponent onClick={handleClick} />; 8}

Even though handleClick does the same thing on every render, it's a different function object each time. If ChildComponent is using React.memo or is otherwise comparing props for reference equality, it will re-render unnecessarily.

useCallback vs. Regular Functions

Let's compare the two approaches:

Without useCallback:

1function ParentComponent() { 2 const [count, setCount] = useState(0); 3 4 // A new function reference is created on every render 5 const handleIncrement = () => { 6 setCount(count + 1); 7 }; 8 9 return ( 10 <div> 11 <p>Count: {count}</p> 12 <button onClick={() => setCount(count + 1)}> 13 Increment 14 </button> 15 <ExpensiveChild onIncrement={handleIncrement} /> 16 </div> 17 ); 18} 19 20// Using React.memo to prevent unnecessary renders 21const ExpensiveChild = React.memo(({ onIncrement }) => { 22 console.log("ExpensiveChild rendered"); 23 return <button onClick={onIncrement}>Child Increment</button>; 24});

In this example, ExpensiveChild will re-render on every render of ParentComponent because handleIncrement is a new function each time.

With useCallback:

1function ParentComponent() { 2 const [count, setCount] = useState(0); 3 4 // The function reference remains the same between renders 5 // as long as count doesn't change 6 const handleIncrement = useCallback(() => { 7 setCount(count + 1); 8 }, [count]); 9 10 return ( 11 <div> 12 <p>Count: {count}</p> 13 <button onClick={() => setCount(count + 1)}> 14 Increment 15 </button> 16 <ExpensiveChild onIncrement={handleIncrement} /> 17 </div> 18 ); 19} 20 21// Using React.memo to prevent unnecessary renders 22const ExpensiveChild = React.memo(({ onIncrement }) => { 23 console.log("ExpensiveChild rendered"); 24 return <button onClick={onIncrement}>Child Increment</button>; 25});

Now, ExpensiveChild will only re-render when count changes, because the handleIncrement function's reference remains stable between renders.

When to Use useCallback

You should consider using useCallback when:

  1. You're passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
  2. The callback is a dependency of other hooks like useEffect or useMemo
  3. The function is complex or creates closures over values that could change

Common Use Cases for useCallback

1. Optimizing Child Component Renders

1function SearchComponent() { 2 const [query, setQuery] = useState(''); 3 const [results, setResults] = useState([]); 4 5 // Memoize the search function 6 const handleSearch = useCallback(async (searchQuery) => { 7 const data = await fetchSearchResults(searchQuery); 8 setResults(data); 9 }, []); // Empty deps means this function never changes 10 11 return ( 12 <div> 13 <SearchInput 14 value={query} 15 onChange={setQuery} 16 onSearch={() => handleSearch(query)} 17 /> 18 <MemoizedResultsList results={results} /> 19 </div> 20 ); 21} 22 23// ResultsList is wrapped in React.memo to prevent re-renders when props don't change 24const MemoizedResultsList = React.memo(ResultsList);

2. Preventing Effect Re-runs

1function DataFetcher({ itemId }) { 2 const [data, setData] = useState(null); 3 4 // Memoize the fetch function so it doesn't change on every render 5 const fetchData = useCallback(async () => { 6 const result = await fetch(`/api/items/${itemId}`); 7 const json = await result.json(); 8 setData(json); 9 }, [itemId]); // Only changes when itemId changes 10 11 // Now we can use fetchData as a dependency in useEffect 12 useEffect(() => { 13 fetchData(); 14 15 // Set up polling 16 const intervalId = setInterval(() => { 17 fetchData(); 18 }, 10000); 19 20 return () => clearInterval(intervalId); 21 }, [fetchData]); // Dependencies include our stable callback 22 23 return data ? <ItemDisplay data={data} /> : <Loading />; 24}

3. Event Handlers with Props or State

1function FormWithValidation({ onSubmit }) { 2 const [values, setValues] = useState({ name: '', email: '' }); 3 const [errors, setErrors] = useState({}); 4 5 // Memoize the validation function 6 const validate = useCallback(() => { 7 const newErrors = {}; 8 9 if (!values.name) newErrors.name = 'Name is required'; 10 if (!values.email) newErrors.email = 'Email is required'; 11 else if (!/\S+@\S+\.\S+/.test(values.email)) { 12 newErrors.email = 'Email is invalid'; 13 } 14 15 setErrors(newErrors); 16 return Object.keys(newErrors).length === 0; 17 }, [values]); 18 19 // Memoize the submit handler 20 const handleSubmit = useCallback((e) => { 21 e.preventDefault(); 22 if (validate()) { 23 onSubmit(values); 24 } 25 }, [validate, values, onSubmit]); 26 27 return ( 28 <form onSubmit={handleSubmit}> 29 {/* Form fields */} 30 </form> 31 ); 32}

useCallback and useMemo

  • useCallback returns a memoized function
  • useMemo returns a memoized value (the result of calling a function)

Use useCallback when you want to memoize a function itself, and useMemo when you want to memoize the result of a function.

1// Memoize a function 2const handleClick = useCallback(() => { 3 console.log('Button clicked!'); 4}, []); 5 6// Memoize a calculated value 7const expensiveValue = useMemo(() => { 8 return computeExpensiveValue(a, b); 9}, [a, b]);

Using Functional Updates with useCallback

When your callback function depends on the previous state, it's better to use the functional update form to avoid adding the state value as a dependency:

Less optimal approach:

1const [count, setCount] = useState(0); 2 3// This needs count as a dependency 4const increment = useCallback(() => { 5 setCount(count + 1); 6}, [count]); // count dependency means function changes when count changes

Better approach:

1const [count, setCount] = useState(0); 2 3// This doesn't need count as a dependency 4const increment = useCallback(() => { 5 setCount(prevCount => prevCount + 1); 6}, []); // No dependencies means function is stable

Common Patterns and Best Practices

1. Event Handlers with Parameters

1function List({ items }) { 2 // Memoize the handler with setItem as parameter 3 const handleItemClick = useCallback((id) => { 4 console.log(`Item ${id} clicked`); 5 }, []); 6 7 return ( 8 <ul> 9 {items.map(item => ( 10 <ListItem 11 key={item.id} 12 item={item} 13 // Pass the memoized function with the item id 14 onClick={() => handleItemClick(item.id)} 15 /> 16 ))} 17 </ul> 18 ); 19} 20 21// This component is optimized to not re-render when parent re-renders 22const ListItem = React.memo(({ item, onClick }) => { 23 return <li onClick={onClick}>{item.name}</li>; 24});

2. Debounced Event Handlers

1function SearchInput() { 2 const [query, setQuery] = useState(''); 3 4 // Create a stable reference to the search function 5 const debouncedSearch = useCallback( 6 debounce((searchTerm) => { 7 // Perform search 8 console.log('Searching for:', searchTerm); 9 }, 500), 10 [] // This function never changes 11 ); 12 13 const handleChange = (e) => { 14 const value = e.target.value; 15 setQuery(value); 16 debouncedSearch(value); 17 }; 18 19 return ( 20 <input 21 type="text" 22 value={query} 23 onChange={handleChange} 24 placeholder="Search..." 25 /> 26 ); 27} 28 29// Simple debounce implementation 30function debounce(fn, delay) { 31 let timeoutId; 32 return function(...args) { 33 clearTimeout(timeoutId); 34 timeoutId = setTimeout(() => { 35 fn(...args); 36 }, delay); 37 }; 38}

3. React.memo with useCallback

1const ExpensiveComponent = React.memo(function ExpensiveComponent({ onSave, data }) { 2 // Complex rendering logic 3 return ( 4 <div> 5 <h2>{data.title}</h2> 6 <p>{data.description}</p> 7 <button onClick={() => onSave(data)}>Save</button> 8 </div> 9 ); 10}); 11 12function Container() { 13 const [items, setItems] = useState([/* initial data */]); 14 15 // Stable save handler 16 const handleSave = useCallback((item) => { 17 // Save logic 18 console.log('Saving:', item); 19 // Update items 20 setItems(prevItems => { 21 return prevItems.map(i => 22 i.id === item.id ? {...i, saved: true} : i 23 ); 24 }); 25 }, []); // No dependencies, function remains stable across renders 26 27 return ( 28 <div> 29 {items.map(item => ( 30 <ExpensiveComponent 31 key={item.id} 32 data={item} 33 onSave={handleSave} 34 /> 35 ))} 36 </div> 37 ); 38}

Performance Considerations

Like with any optimization technique, useCallback comes with tradeoffs:

  1. Memory Usage: Memoizing functions consumes a little more memory
  2. Dependency Tracking: React still has to check the dependencies on every render
  3. Complexity: Overusing useCallback can make your code harder to read and maintain

When Not to Use useCallback

Don't use useCallback for every function in your component by default. Only use it when you've identified a specific performance issue or when:

  1. A function is passed to a component wrapped in React.memo
  2. A function is used as a dependency in hooks like useEffect
  3. A function is expensive to create or has internal state (e.g., debounced functions)

Advanced useCallback with Refs

You can combine useCallback with useRef to maintain function identity across renders while still accessing the latest props or state:

1function SearchComponent({ apiKey }) { 2 const [query, setQuery] = useState(''); 3 const apiKeyRef = useRef(apiKey); 4 5 // Update ref when apiKey changes 6 useEffect(() => { 7 apiKeyRef.current = apiKey; 8 }, [apiKey]); 9 10 // This function never changes identity 11 const search = useCallback(() => { 12 // Access latest apiKey via ref 13 const currentApiKey = apiKeyRef.current; 14 // Perform search with query and apiKey 15 console.log(`Searching for "${query}" with key ${currentApiKey}`); 16 }, [query]); // Only depends on query, not apiKey 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 </div> 26 ); 27}

Complex Example: Data Grid with Sorting and Filtering

1import React, { useState, useCallback, useMemo } from 'react'; 2 3function DataGrid({ data, columns }) { 4 const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); 5 const [filters, setFilters] = useState({}); 6 7 // Memoize the sort function 8 const handleSort = useCallback((columnKey) => { 9 setSortConfig(prevSort => { 10 if (prevSort.key === columnKey) { 11 // Toggle direction if same column 12 return { 13 key: columnKey, 14 direction: prevSort.direction === 'asc' ? 'desc' : 'asc' 15 }; 16 } 17 18 // Default to ascending for new column 19 return { key: columnKey, direction: 'asc' }; 20 }); 21 }, []); 22 23 // Memoize the filter function 24 const handleFilterChange = useCallback((columnKey, value) => { 25 setFilters(prevFilters => ({ 26 ...prevFilters, 27 [columnKey]: value 28 })); 29 }, []); 30 31 // Clear all filters 32 const clearFilters = useCallback(() => { 33 setFilters({}); 34 }, []); 35 36 // Apply sorting and filtering with useMemo 37 const processedData = useMemo(() => { 38 // First apply filters 39 let result = data.filter(item => { 40 // Check each filter 41 return Object.entries(filters).every(([key, value]) => { 42 if (!value) return true; // Skip empty filters 43 44 const itemValue = String(item[key] || '').toLowerCase(); 45 return itemValue.includes(String(value).toLowerCase()); 46 }); 47 }); 48 49 // Then sort 50 if (sortConfig.key) { 51 result = [...result].sort((a, b) => { 52 const aValue = a[sortConfig.key]; 53 const bValue = b[sortConfig.key]; 54 55 if (aValue < bValue) { 56 return sortConfig.direction === 'asc' ? -1 : 1; 57 } 58 if (aValue > bValue) { 59 return sortConfig.direction === 'asc' ? 1 : -1; 60 } 61 return 0; 62 }); 63 } 64 65 return result; 66 }, [data, filters, sortConfig]); 67 68 return ( 69 <div className="data-grid"> 70 <div className="filters"> 71 {columns.map(column => ( 72 <div key={column.key} className="filter"> 73 <label>{column.label} Filter:</label> 74 <input 75 value={filters[column.key] || ''} 76 onChange={e => handleFilterChange(column.key, e.target.value)} 77 placeholder={`Filter by ${column.label}`} 78 /> 79 </div> 80 ))} 81 <button onClick={clearFilters}>Clear Filters</button> 82 </div> 83 84 <table> 85 <thead> 86 <tr> 87 {columns.map(column => ( 88 <th 89 key={column.key} 90 onClick={() => handleSort(column.key)} 91 className={sortConfig.key === column.key ? sortConfig.direction : ''} 92 > 93 {column.label} 94 {sortConfig.key === column.key && ( 95 <span>{sortConfig.direction === 'asc' ? ' ▲' : ' ▼'}</span> 96 )} 97 </th> 98 ))} 99 </tr> 100 </thead> 101 <tbody> 102 {processedData.map((row, index) => ( 103 <tr key={row.id || index}> 104 {columns.map(column => ( 105 <td key={column.key}>{row[column.key]}</td> 106 ))} 107 </tr> 108 ))} 109 </tbody> 110 </table> 111 112 <div className="summary"> 113 Showing {processedData.length} of {data.length} rows 114 </div> 115 </div> 116 ); 117} 118 119// Usage 120function App() { 121 const columns = [ 122 { key: 'name', label: 'Name' }, 123 { key: 'age', label: 'Age' }, 124 { key: 'occupation', label: 'Occupation' } 125 ]; 126 127 const data = [ 128 { id: 1, name: 'John Doe', age: 28, occupation: 'Developer' }, 129 { id: 2, name: 'Jane Smith', age: 32, occupation: 'Designer' }, 130 { id: 3, name: 'Bob Johnson', age: 45, occupation: 'Manager' }, 131 // More data... 132 ]; 133 134 return <DataGrid data={data} columns={columns} />; 135}

In this complex example:

  1. We use useCallback to memoize event handlers for sorting and filtering
  2. We use useMemo to process the data based on current sort and filter settings
  3. Components only recalculate what's necessary when specific pieces of state change

Conclusion

The useCallback hook is a valuable tool for optimizing React applications by memoizing functions to prevent unnecessary re-renders. It's especially useful when:

  • Passing callback functions to optimized child components
  • Using functions as dependencies in other hooks
  • Creating stable function references that maintain identity between renders

Key takeaways:

  • Use useCallback to memoize functions that should maintain the same reference between renders
  • Only include dependencies that the function actually uses
  • Consider using the functional update pattern to avoid unnecessary dependencies
  • Combine with React.memo to prevent unnecessary child component re-renders
  • Don't overuse it—only apply useCallback when there's a clear performance benefit

Remember that premature optimization can lead to more complex code without meaningful performance gains. Use useCallback strategically when you've identified specific performance issues or in the scenarios outlined in this lesson.