Back to Blog
ReactmemoPerformanceOptimization

React.memo Guide

Master React.memo for optimizing functional component performance.

B
Bootspring Team
Engineering
August 4, 2018
6 min read

React.memo is a higher-order component that memoizes functional components, preventing unnecessary re-renders when props haven't changed.

Basic Usage#

1import { memo } from 'react'; 2 3// Without memo - re-renders when parent re-renders 4function ExpensiveList({ items }) { 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 when items changes 16const MemoizedList = memo(function ExpensiveList({ items }) { 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// Usage 28function Parent() { 29 const [count, setCount] = useState(0); 30 const items = useMemo(() => [{ id: 1, name: 'Item 1' }], []); 31 32 return ( 33 <div> 34 <button onClick={() => setCount(c => c + 1)}> 35 Count: {count} 36 </button> 37 <MemoizedList items={items} /> 38 {/* MemoizedList won't re-render when count changes */} 39 </div> 40 ); 41}

How memo Works#

1// memo performs shallow comparison of props 2const User = memo(function User({ name, age, settings }) { 3 console.log('User rendered'); 4 return ( 5 <div> 6 <p>Name: {name}</p> 7 <p>Age: {age}</p> 8 </div> 9 ); 10}); 11 12function Parent() { 13 const [count, setCount] = useState(0); 14 15 // Primitive props - stable between renders 16 const name = 'John'; // Same reference 17 const age = 30; // Same value 18 19 // Object prop - NEW reference each render! 20 const settings = { theme: 'dark' }; 21 22 return ( 23 <div> 24 <button onClick={() => setCount(c => c + 1)}> 25 Increment 26 </button> 27 {/* User re-renders because settings is new object! */} 28 <User name={name} age={age} settings={settings} /> 29 </div> 30 ); 31} 32 33// Fix with useMemo 34function ParentFixed() { 35 const [count, setCount] = useState(0); 36 37 const settings = useMemo(() => ({ theme: 'dark' }), []); 38 39 return ( 40 <div> 41 <button onClick={() => setCount(c => c + 1)}> 42 Increment 43 </button> 44 {/* Now User doesn't re-render on count change */} 45 <User name="John" age={30} settings={settings} /> 46 </div> 47 ); 48}

Custom Comparison Function#

1// Provide custom comparison 2const User = memo( 3 function User({ user, onUpdate }) { 4 console.log('User rendered'); 5 return ( 6 <div> 7 <p>{user.name}</p> 8 <button onClick={onUpdate}>Update</button> 9 </div> 10 ); 11 }, 12 // Custom areEqual function 13 (prevProps, nextProps) => { 14 // Return true if props are equal (skip re-render) 15 // Return false if props are different (re-render) 16 return ( 17 prevProps.user.id === nextProps.user.id && 18 prevProps.user.name === nextProps.user.name 19 ); 20 } 21); 22 23// Deep comparison (use carefully - can be expensive) 24import isEqual from 'lodash/isEqual'; 25 26const DeepComparedComponent = memo( 27 function Component({ data }) { 28 return <div>{JSON.stringify(data)}</div>; 29 }, 30 (prevProps, nextProps) => isEqual(prevProps, nextProps) 31);

Common Pitfalls#

1// PITFALL 1: New object/array props 2function Parent() { 3 return ( 4 // ❌ New array created each render 5 <MemoizedList items={[1, 2, 3]} /> 6 7 // ✓ Use useMemo or define outside 8 <MemoizedList items={useMemo(() => [1, 2, 3], [])} /> 9 ); 10} 11 12// PITFALL 2: Inline function props 13function Parent() { 14 return ( 15 // ❌ New function each render 16 <MemoizedButton onClick={() => console.log('clicked')} /> 17 18 // ✓ Use useCallback 19 <MemoizedButton onClick={useCallback(() => console.log('clicked'), [])} /> 20 ); 21} 22 23// PITFALL 3: Children prop 24function Parent() { 25 return ( 26 // ❌ Children are new elements each render 27 <MemoizedWrapper> 28 <Child /> 29 </MemoizedWrapper> 30 ); 31} 32 33// ✓ Memoize children too 34const MemoizedChild = memo(Child); 35function Parent() { 36 return ( 37 <MemoizedWrapper> 38 <MemoizedChild /> 39 </MemoizedWrapper> 40 ); 41} 42 43// PITFALL 4: Context changes 44const ThemeContext = createContext(); 45 46const MemoizedComponent = memo(function Component({ name }) { 47 const theme = useContext(ThemeContext); 48 // Re-renders when theme changes, regardless of memo! 49 return <div className={theme}>{name}</div>; 50});

Memoizing with Callbacks#

1function Parent() { 2 const [items, setItems] = useState([]); 3 4 // ✓ Stable callback reference 5 const handleDelete = useCallback((id) => { 6 setItems(prev => prev.filter(item => item.id !== id)); 7 }, []); 8 9 // ✓ Stable callback with item dependency 10 const handleUpdate = useCallback((id, data) => { 11 setItems(prev => 12 prev.map(item => 13 item.id === id ? { ...item, ...data } : item 14 ) 15 ); 16 }, []); 17 18 return ( 19 <ul> 20 {items.map(item => ( 21 <MemoizedItem 22 key={item.id} 23 item={item} 24 onDelete={handleDelete} 25 onUpdate={handleUpdate} 26 /> 27 ))} 28 </ul> 29 ); 30} 31 32const MemoizedItem = memo(function Item({ item, onDelete, onUpdate }) { 33 console.log(`Item ${item.id} rendered`); 34 return ( 35 <li> 36 {item.name} 37 <button onClick={() => onDelete(item.id)}>Delete</button> 38 </li> 39 ); 40});

When to Use memo#

1// ✓ USE memo when: 2 3// 1. Component renders often with same props 4const FrequentlyRenderedChild = memo(function Child({ data }) { 5 return <div>{data}</div>; 6}); 7 8// 2. Component has expensive rendering 9const ExpensiveComponent = memo(function Expensive({ items }) { 10 const processed = items.map(item => { 11 // Heavy computation 12 return complexCalculation(item); 13 }); 14 return <ul>{processed.map(p => <li key={p.id}>{p.value}</li>)}</ul>; 15}); 16 17// 3. Parent re-renders frequently but child props are stable 18function FastParent() { 19 const [count, setCount] = useState(0); 20 const stableData = useMemo(() => ({ id: 1 }), []); 21 22 return ( 23 <div> 24 <button onClick={() => setCount(c => c + 1)}>{count}</button> 25 <MemoizedChild data={stableData} /> 26 </div> 27 ); 28} 29 30// ❌ DON'T use memo when: 31 32// 1. Props change on every render anyway 33function AlwaysChanging({ timestamp }) { 34 return <div>{timestamp}</div>; 35} 36 37// 2. Component is simple/cheap to render 38function SimpleText({ text }) { 39 return <span>{text}</span>; 40} 41 42// 3. It makes code significantly more complex 43// (premature optimization)

Nested Memoization#

1// Memoize at multiple levels 2const App = memo(function App() { 3 const [filter, setFilter] = useState(''); 4 const [sort, setSort] = useState('name'); 5 6 return ( 7 <div> 8 <MemoizedHeader /> 9 <MemoizedFilters 10 filter={filter} 11 setFilter={setFilter} 12 sort={sort} 13 setSort={setSort} 14 /> 15 <MemoizedContent filter={filter} sort={sort} /> 16 <MemoizedFooter /> 17 </div> 18 ); 19}); 20 21const MemoizedHeader = memo(function Header() { 22 // Only renders once 23 return <header>My App</header>; 24}); 25 26const MemoizedFilters = memo(function Filters({ 27 filter, setFilter, sort, setSort 28}) { 29 // Renders when filter or sort changes 30 return ( 31 <div> 32 <input value={filter} onChange={e => setFilter(e.target.value)} /> 33 <select value={sort} onChange={e => setSort(e.target.value)}> 34 <option value="name">Name</option> 35 <option value="date">Date</option> 36 </select> 37 </div> 38 ); 39});

Debugging Memoization#

1// Add logging to understand re-renders 2const MemoizedComponent = memo( 3 function Component({ data }) { 4 console.log('Component rendered', data); 5 return <div>{data.name}</div>; 6 }, 7 (prevProps, nextProps) => { 8 const areEqual = prevProps.data === nextProps.data; 9 console.log('Props comparison:', { 10 prev: prevProps.data, 11 next: nextProps.data, 12 areEqual 13 }); 14 return areEqual; 15 } 16); 17 18// Use React DevTools Profiler 19// - Highlights components that re-rendered 20// - Shows why component re-rendered 21// - Measures render duration 22 23// why-did-you-render library 24import whyDidYouRender from '@welldone-software/why-did-you-render'; 25 26if (process.env.NODE_ENV === 'development') { 27 whyDidYouRender(React, { 28 trackAllPureComponents: true 29 }); 30}

Best Practices#

When to Memoize: ✓ Expensive rendering ✓ Frequent parent re-renders ✓ Stable props (primitives, memoized values) ✓ Part of a list with many items Props Stability: ✓ Use useMemo for objects/arrays ✓ Use useCallback for functions ✓ Extract constants outside component ✓ Pass primitives when possible Custom Comparison: ✓ Only when default comparison fails ✓ Keep comparison fast ✓ Avoid deep comparison for large objects ✓ Document why custom comparison is needed Avoid: ✗ Memoizing everything ✗ New objects/functions in props ✗ Ignoring context changes ✗ Complex comparison functions

Conclusion#

React.memo prevents unnecessary re-renders by memoizing functional components. It performs shallow prop comparison by default, so ensure object and function props have stable references using useMemo and useCallback. Use memo when components are expensive to render or have stable props while parents re-render frequently. Avoid over-optimization - profile first, then memoize where it matters.

Share this article

Help spread the word about Bootspring