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.