Chapter 1: useCallback and useMemo

The Problem: Unnecessary Re-renders

Every time a component re-renders, all its code is re-executed:

function ProductList({ products }) {
    // Recalculated on EVERY render, even if products hasn't changed
    const sortedProducts = products.sort((a, b) => a.price - b.price);

    // New function created on EVERY render
    const handleClick = (id) => {
        console.log('Clicked:', id);
    };

    return (
        <ul>
            {sortedProducts.map(p => (
                <ProductItem key={p.id} product={p} onClick={handleClick} />
            ))}
        </ul>
    );
}

If products contains 1000 elements, the sort is done on every render, even if nothing has changed.

useMemo: Memoize a Value

useMemo memoizes the result of a calculation and only recalculates it when dependencies change.

Syntax

const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);

Example: Expensive Calculation

import { useMemo } from 'react';

function ProductList({ products, sortBy }) {
    // Recalculated ONLY if products or sortBy changes
    const sortedProducts = useMemo(() => {
        console.log('Sorting...');
        return [...products].sort((a, b) => {
            if (sortBy === 'price') return a.price - b.price;
            if (sortBy === 'name') return a.name.localeCompare(b.name);
            return 0;
        });
    }, [products, sortBy]);

    return (
        <ul>
            {sortedProducts.map(p => (
                <li key={p.id}>{p.name} - {p.price}€</li>
            ))}
        </ul>
    );
}

Example: Filtering

function UserList({ users, searchTerm }) {
    const filteredUsers = useMemo(() => {
        return users.filter(user =>
            user.name.toLowerCase().includes(searchTerm.toLowerCase())
        );
    }, [users, searchTerm]);

    return (
        <ul>
            {filteredUsers.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

useCallback: Memoize a Function

useCallback memoizes a function and only recreates it when dependencies change.

Syntax

const memoizedFn = useCallback(() => {
    doSomething(a, b);
}, [a, b]);

Why Is This Useful?

In JavaScript, every time a function is defined, it's a new reference:

const fn1 = () => console.log('hello');
const fn2 = () => console.log('hello');

fn1 === fn2;  // false! They are different functions

This causes problems with React.memo and useEffect dependencies.

Example with React.memo

// Memoized child component
const ProductItem = React.memo(function ProductItem({ product, onSelect }) {
    console.log('ProductItem rendered:', product.name);
    return (
        <li onClick={() => onSelect(product.id)}>
            {product.name}
        </li>
    );
});

// Parent component
function ProductList({ products }) {
    const [selected, setSelected] = useState(null);

    // WITHOUT useCallback: new function on every render
    // -> ProductItem re-renders even if product hasn't changed
    const handleSelect = (id) => {
        setSelected(id);
    };

    // WITH useCallback: same function as long as dependencies don't change
    const handleSelectMemo = useCallback((id) => {
        setSelected(id);
    }, []);  // [] because setSelected is stable

    return (
        <ul>
            {products.map(p => (
                <ProductItem
                    key={p.id}
                    product={p}
                    onSelect={handleSelectMemo}
                />
            ))}
        </ul>
    );
}

Example with Dependencies

function SearchForm({ onSearch, category }) {
    const [query, setQuery] = useState('');

    // The function depends on category
    const handleSubmit = useCallback(() => {
        onSearch(query, category);
    }, [onSearch, query, category]);

    return (
        <form onSubmit={handleSubmit}>
            <input
                value={query}
                onChange={(e) => setQuery(e.target.value)}
            />
            <button type="submit">Search</button>
        </form>
    );
}

React.memo: Memoize a Component

React.memo prevents a component from re-rendering if its props haven't changed.

const MyComponent = React.memo(function MyComponent({ name, onClick }) {
    console.log('MyComponent rendered');
    return <button onClick={onClick}>{name}</button>;
});

Important: React.memo compares props by reference. If a function is recreated on every render, the component will still re-render.

When to Use useMemo?

YES:

  • Expensive calculations (sorting, filtering large lists)
  • Creating objects passed as props to memoized components
  • Calculations whose result is used in multiple places

NO:

  • Simple calculations (addition, concatenation)
  • Components that re-render anyway
  • "Just in case" by default

When to Use useCallback?

YES:

  • Functions passed to memoized child components (React.memo)
  • Functions in useEffect dependencies
  • Expensive event handlers

NO:

  • Functions used only locally
  • Child components not memoized
  • "For optimization" by default

Comparison

Hook Memoizes Recalculates if
useMemo A value Dependencies change
useCallback A function Dependencies change
React.memo A component Props change

Pitfall: Incorrect Dependencies

// PROBLEM: items is missing from dependencies
const getTotal = useCallback(() => {
    return items.reduce((sum, item) => sum + item.price, 0);
}, []);  // items should be in the array!

// CORRECT
const getTotal = useCallback(() => {
    return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);

ESLint with the eslint-plugin-react-hooks plugin will warn you about missing dependencies.

Exercises

Exercise 1: Optimized Filtered List

Create a product list with a search field. Optimize to not re-filter if the search hasn't changed.

Solution:

function OptimizedProductList({ products }) {
    const [search, setSearch] = useState('');
    const [sortBy, setSortBy] = useState('name');

    const filteredAndSorted = useMemo(() => {
        console.log('Filtering and sorting...');
        return products
            .filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
            .sort((a, b) => {
                if (sortBy === 'name') return a.name.localeCompare(b.name);
                if (sortBy === 'price') return a.price - b.price;
                return 0;
            });
    }, [products, search, sortBy]);

    return (
        <div>
            <input
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Search..."
            />
            <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
                <option value="name">By name</option>
                <option value="price">By price</option>
            </select>
            <ul>
                {filteredAndSorted.map(p => (
                    <li key={p.id}>{p.name} - {p.price}€</li>
                ))}
            </ul>
        </div>
    );
}

Exercise 2: Stable Callback

Create a parent component with multiple child buttons. Optimize so buttons don't re-render unnecessarily.

Solution:

const Button = React.memo(function Button({ id, onClick, children }) {
    console.log('Button rendered:', id);
    return <button onClick={() => onClick(id)}>{children}</button>;
});

function ButtonGroup() {
    const [clicked, setClicked] = useState(null);

    const handleClick = useCallback((id) => {
        setClicked(id);
        console.log('Button clicked:', id);
    }, []);

    return (
        <div>
            <p>Last click: {clicked}</p>
            <Button id="a" onClick={handleClick}>Button A</Button>
            <Button id="b" onClick={handleClick}>Button B</Button>
            <Button id="c" onClick={handleClick}>Button C</Button>
        </div>
    );
}

Key Points to Remember

  1. useMemo memoizes a calculated value
  2. useCallback memoizes a function
  3. React.memo memoizes a component
  4. Don't optimize prematurely - measure first
  5. Complete dependencies - always list all used values

<- Back to module | Next Chapter: Custom Hooks ->