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.