Back to Blog
ReactPerformanceHooksOptimization

React.memo, useCallback, and useMemo Explained

Master React performance optimization with memo, useCallback, and useMemo. When and how to use each.

B
Bootspring Team
Engineering
October 28, 2020
7 min read

Understanding when to use these optimization tools is crucial for React performance. Here's a comprehensive guide.

React.memo Basics#

1import { memo } from 'react'; 2 3// Without memo - re-renders when parent renders 4function ExpensiveList({ items }: { items: Item[] }) { 5 console.log('ExpensiveList rendered'); 6 return ( 7 <ul> 8 {items.map(item => ( 9 <li key={item.id}>{item.name}</li> 10 ))} 11 </ul> 12 ); 13} 14 15// With memo - only re-renders if props change 16const MemoizedList = memo(function ExpensiveList({ items }: { items: Item[] }) { 17 console.log('MemoizedList rendered'); 18 return ( 19 <ul> 20 {items.map(item => ( 21 <li key={item.id}>{item.name}</li> 22 ))} 23 </ul> 24 ); 25}); 26 27// Custom comparison function 28const DeepMemoizedList = memo( 29 function ExpensiveList({ items }: { items: Item[] }) { 30 return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>; 31 }, 32 (prevProps, nextProps) => { 33 // Return true if props are equal (skip re-render) 34 return prevProps.items.length === nextProps.items.length && 35 prevProps.items.every((item, i) => item.id === nextProps.items[i].id); 36 } 37);

useCallback Basics#

1import { useCallback, useState } from 'react'; 2 3function ParentComponent() { 4 const [count, setCount] = useState(0); 5 const [name, setName] = useState(''); 6 7 // Without useCallback - new function every render 8 const handleClick = () => { 9 console.log('Button clicked'); 10 }; 11 12 // With useCallback - same function reference 13 const handleClickMemoized = useCallback(() => { 14 console.log('Button clicked'); 15 }, []); // Empty deps = never changes 16 17 // With dependencies 18 const handleIncrement = useCallback(() => { 19 setCount(c => c + 1); 20 }, []); // Using updater, no deps needed 21 22 const handleReset = useCallback(() => { 23 setCount(0); 24 setName(''); 25 }, []); // No external values used 26 27 return ( 28 <div> 29 <MemoizedButton onClick={handleClickMemoized} /> 30 <MemoizedButton onClick={handleIncrement} /> 31 </div> 32 ); 33} 34 35// This only matters with memo 36const MemoizedButton = memo(function Button({ 37 onClick 38}: { 39 onClick: () => void 40}) { 41 console.log('Button rendered'); 42 return <button onClick={onClick}>Click</button>; 43});

useMemo Basics#

1import { useMemo, useState } from 'react'; 2 3function ExpensiveComponent({ items, filter }: Props) { 4 // Without useMemo - computed every render 5 const filteredItems = items.filter(item => item.name.includes(filter)); 6 7 // With useMemo - only recomputed when deps change 8 const memoizedFilteredItems = useMemo(() => { 9 console.log('Filtering items...'); 10 return items.filter(item => item.name.includes(filter)); 11 }, [items, filter]); 12 13 // Complex computation 14 const statistics = useMemo(() => { 15 console.log('Computing statistics...'); 16 return { 17 total: items.length, 18 filtered: memoizedFilteredItems.length, 19 average: items.reduce((a, b) => a + b.value, 0) / items.length, 20 }; 21 }, [items, memoizedFilteredItems]); 22 23 return ( 24 <div> 25 <Stats data={statistics} /> 26 <List items={memoizedFilteredItems} /> 27 </div> 28 ); 29}

When NOT to Use These#

1// DON'T: Memoize cheap operations 2function SimpleComponent({ name }: { name: string }) { 3 // Unnecessary - string concatenation is cheap 4 const greeting = useMemo(() => `Hello, ${name}!`, [name]); 5 6 // Just do this instead 7 const greeting2 = `Hello, ${name}!`; 8 9 return <h1>{greeting2}</h1>; 10} 11 12// DON'T: Memoize callbacks not passed to memoized children 13function Parent() { 14 // Unnecessary - Child is not memoized 15 const handleClick = useCallback(() => { 16 console.log('clicked'); 17 }, []); 18 19 // Child will re-render anyway when Parent re-renders 20 return <Child onClick={handleClick} />; 21} 22 23// DON'T: Premature optimization 24function NotExpensive({ items }: { items: string[] }) { 25 // Array.map is fast for reasonable sizes 26 return ( 27 <ul> 28 {items.map(item => <li key={item}>{item}</li>)} 29 </ul> 30 ); 31}

When TO Use These#

1// DO: Expensive computations 2function DataGrid({ data, sortConfig }: Props) { 3 const sortedData = useMemo(() => { 4 // Sorting large arrays is expensive 5 return [...data].sort((a, b) => { 6 return sortConfig.direction === 'asc' 7 ? a[sortConfig.key] - b[sortConfig.key] 8 : b[sortConfig.key] - a[sortConfig.key]; 9 }); 10 }, [data, sortConfig]); 11 12 return <Table data={sortedData} />; 13} 14 15// DO: Callbacks passed to memoized children 16function TodoApp() { 17 const [todos, setTodos] = useState<Todo[]>([]); 18 19 const handleToggle = useCallback((id: string) => { 20 setTodos(prev => prev.map(todo => 21 todo.id === id ? { ...todo, done: !todo.done } : todo 22 )); 23 }, []); 24 25 return <MemoizedTodoList todos={todos} onToggle={handleToggle} />; 26} 27 28// DO: Preserve referential equality for effects 29function SearchComponent({ query }: { query: string }) { 30 const searchParams = useMemo(() => ({ 31 q: query, 32 limit: 10, 33 offset: 0, 34 }), [query]); 35 36 useEffect(() => { 37 // Only runs when searchParams actually changes 38 fetchResults(searchParams); 39 }, [searchParams]); 40 41 return <Results />; 42}

Common Patterns#

1// Pattern: Memoized context value 2function ThemeProvider({ children }: { children: ReactNode }) { 3 const [theme, setTheme] = useState('light'); 4 5 // Memoize to prevent context consumers from re-rendering 6 const value = useMemo(() => ({ 7 theme, 8 toggle: () => setTheme(t => t === 'light' ? 'dark' : 'light'), 9 }), [theme]); 10 11 return ( 12 <ThemeContext.Provider value={value}> 13 {children} 14 </ThemeContext.Provider> 15 ); 16} 17 18// Pattern: Stable callbacks in lists 19function ItemList({ items, onSelect }: Props) { 20 return ( 21 <ul> 22 {items.map(item => ( 23 <MemoizedItem 24 key={item.id} 25 item={item} 26 onSelect={onSelect} // Must be stable! 27 /> 28 ))} 29 </ul> 30 ); 31} 32 33const MemoizedItem = memo(function Item({ item, onSelect }: ItemProps) { 34 return ( 35 <li onClick={() => onSelect(item.id)}> 36 {item.name} 37 </li> 38 ); 39}); 40 41// Pattern: Derived state 42function FilteredList({ items, searchTerm }: Props) { 43 const filtered = useMemo(() => 44 items.filter(item => 45 item.name.toLowerCase().includes(searchTerm.toLowerCase()) 46 ), 47 [items, searchTerm] 48 ); 49 50 const grouped = useMemo(() => 51 filtered.reduce((acc, item) => { 52 const key = item.category; 53 acc[key] = acc[key] || []; 54 acc[key].push(item); 55 return acc; 56 }, {} as Record<string, Item[]>), 57 [filtered] 58 ); 59 60 return <GroupedList groups={grouped} />; 61}

Fixing Common Mistakes#

1// MISTAKE: Object dependency creates new reference 2function BadExample({ userId }: { userId: string }) { 3 const fetchUser = useCallback(() => { 4 return api.getUser({ id: userId }); // Creates new object! 5 }, [userId]); // This works, but... 6 7 // BETTER: Avoid inline objects 8 const fetchUserBetter = useCallback(() => { 9 return api.getUser(userId); 10 }, [userId]); 11 12 return <UserProfile fetch={fetchUserBetter} />; 13} 14 15// MISTAKE: Memoizing inline objects 16function AlsoBad() { 17 // New object every render despite useMemo 18 return <Child style={useMemo(() => ({ color: 'red' }), [])} />; 19 20 // BETTER: Define outside or use CSS 21} 22 23// MISTAKE: Missing dependencies 24function MissingDeps({ count }: { count: number }) { 25 // Bug: count is stale! 26 const handleClick = useCallback(() => { 27 console.log(count); // Always logs initial count 28 }, []); // Missing count dependency 29 30 // CORRECT: 31 const handleClickFixed = useCallback(() => { 32 console.log(count); 33 }, [count]); 34 35 return <button onClick={handleClickFixed}>Log</button>; 36}

Performance Testing#

1import { Profiler, ProfilerOnRenderCallback } from 'react'; 2 3function App() { 4 const onRender: ProfilerOnRenderCallback = ( 5 id, 6 phase, 7 actualDuration, 8 baseDuration, 9 startTime, 10 commitTime 11 ) => { 12 console.log({ 13 id, 14 phase, 15 actualDuration, 16 baseDuration, 17 }); 18 }; 19 20 return ( 21 <Profiler id="App" onRender={onRender}> 22 <ExpensiveComponent /> 23 </Profiler> 24 ); 25} 26 27// Custom hook to log renders 28function useRenderCount(name: string) { 29 const count = useRef(0); 30 count.current++; 31 32 useEffect(() => { 33 console.log(`${name} rendered ${count.current} times`); 34 }); 35}

Decision Flowchart#

Should I use React.memo? ├── Is the component expensive to render? → Consider memo ├── Does parent re-render often with same props? → Consider memo ├── Are all props primitives? → memo works easily └── Has functions/objects as props? → Need useCallback/useMemo too Should I use useCallback? ├── Is the callback passed to memoized child? → Yes ├── Is the callback used in dependency array? → Yes ├── Is the callback just used locally? → Probably not └── Is child not memoized? → Probably not Should I use useMemo? ├── Is the computation expensive? → Yes ├── Is the result used in dependency array? → Yes ├── Is it creating objects for memoized children? → Yes ├── Is it simple math/string ops? → Probably not └── Is it just JSX creation? → Probably not

Best Practices#

General: ✓ Measure before optimizing ✓ Use React DevTools Profiler ✓ Understand the render cascade ✓ Start without optimization React.memo: ✓ Use for pure components ✓ Combine with useCallback for handlers ✓ Consider custom comparator for complex props ✓ Don't use for frequently changing props useCallback: ✓ Use when passed to memoized children ✓ Use when in dependency arrays ✓ Include all dependencies ✓ Use updater functions to reduce deps useMemo: ✓ Use for expensive computations ✓ Use to maintain referential equality ✓ Don't overuse for simple operations ✓ Consider moving computation outside

Conclusion#

React.memo, useCallback, and useMemo are powerful but often misused. Use them when there's a measurable performance benefit: expensive computations, preventing unnecessary re-renders of memoized children, or maintaining referential equality for effects. Always measure first, and avoid premature optimization.

Share this article

Help spread the word about Bootspring