Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
16 minLesson 9 of 33
Advanced React Patterns

useMemo & useCallback: Performance

useMemo & useCallback: Performance Optimization

React re-renders components when state or props change. Most of the time this is fast enough and you shouldn't think about it. But some operations are expensive — filtering 10,000 items, computing statistics, creating complex objects — and some component trees re-render unnecessarily. useMemo and useCallback are the tools to address both.

The Problem They Solve

function ProductList({ products, searchQuery }: Props) {
    // This runs on EVERY render — even if products and searchQuery didn't change
    const filtered = products
        .filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
        .sort((a, b) => a.price - b.price);
    
    return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

If this component's parent re-renders for unrelated reasons (say, a header updates), filtered gets recalculated from scratch even if products and searchQuery haven't changed. For small datasets this doesn't matter. For thousands of products with complex filtering, it adds up.

useMemo — Memoize Computed Values

useMemo caches a calculated value and only recomputes it when its dependencies change:

import { useMemo } from "react";

function ProductList({ products, searchQuery, category }: Props) {
    const filtered = useMemo(() => {
        return products
            .filter(p => !category || p.category === category)
            .filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
            .sort((a, b) => a.price - b.price);
    }, [products, searchQuery, category]);   // only recompute when these change
    
    return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

The array at the end is the dependency array — identical to useEffect. If any dependency changes, the memo is invalidated and the function runs again.

useCallback — Memoize Functions

Functions are recreated on every render. When a function is passed as a prop to a child component, the child sees a new function reference on every parent render, causing the child to re-render even if nothing actually changed:

// Without useCallback
function Parent() {
    const [count, setCount] = useState(0);
    
    // New function reference on every render
    const handleClick = () => console.log("clicked");
    
    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>Increment {count}</button>
            <ExpensiveChild onClick={handleClick} />
        </div>
    );
}

Every time count changes, Parent re-renders, creating a new handleClick function. If ExpensiveChild uses React.memo, it checks if props changed — but handleClick IS a new function, so it still re-renders.

import { useCallback } from "react";

function Parent() {
    const [count, setCount] = useState(0);
    
    // Same function reference unless dependencies change
    const handleClick = useCallback(() => {
        console.log("clicked");
    }, []);   // empty array = created once
    
    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>Increment {count}</button>
            <ExpensiveChild onClick={handleClick} />
        </div>
    );
}

Now handleClick has the same reference across renders unless its dependencies change. Paired with React.memo on ExpensiveChild, the child genuinely won't re-render.

React.memo — Skip Re-renders When Props Are Unchanged

// Without memo — re-renders whenever parent re-renders
function CourseCard({ course }: { course: Course }) {
    return (
        <div className="card">
            <h3>{course.title}</h3>
            <p>${course.price}</p>
        </div>
    );
}

// With memo — only re-renders if course prop changes
const CourseCard = React.memo(function CourseCard({ course }: { course: Course }) {
    return (
        <div className="card">
            <h3>{course.title}</h3>
            <p>${course.price}</p>
        </div>
    );
});

React.memo does a shallow comparison of props. If all props pass === equality, the component skips rendering. This is where useCallback matters — without it, a function prop will always fail equality even if the function does the same thing.

The Full Optimization Pattern

// A filtered list with many items and an expensive child component

const CourseCard = React.memo(function CourseCard({ 
    course, 
    onEnroll 
}: { 
    course: Course; 
    onEnroll: (id: string) => void;
}) {
    console.log(`Rendering: ${course.title}`);
    return (
        <div>
            <h3>{course.title}</h3>
            <button onClick={() => onEnroll(course.id)}>Enroll</button>
        </div>
    );
});

function CoursesPage({ courses }: { courses: Course[] }) {
    const [search, setSearch] = useState("");
    const [category, setCategory] = useState("all");
    const [cart, setCart] = useState<string[]>([]);
    
    // Memoize the filtered list
    const filtered = useMemo(() => {
        return courses
            .filter(c => category === "all" || c.category === category)
            .filter(c => c.title.toLowerCase().includes(search.toLowerCase()));
    }, [courses, category, search]);
    
    // Memoize the callback (stable reference for React.memo to work)
    const handleEnroll = useCallback((id: string) => {
        setCart(prev => [...prev, id]);
    }, []);   // setCart is stable, no deps needed
    
    return (
        <div>
            <input value={search} onChange={e => setSearch(e.target.value)} />
            <div className="grid grid-cols-3 gap-4">
                {filtered.map(course => (
                    <CourseCard key={course.id} course={course} onEnroll={handleEnroll} />
                ))}
            </div>
        </div>
    );
}

Now: changing search or category recalculates filtered and re-renders the matching cards. Changing cart (after enrolling) does NOT trigger course card re-renders.

When NOT to Use Them

This is important: memoization has a cost. Every useMemo and useCallback adds overhead — React has to store the cached value and compare dependencies on every render.

Don't use useMemo for:

  • Simple calculations (string concatenation, adding two numbers)
  • Values that change as often as they're used (the cache never hits)
  • Components that are already fast

Don't use useCallback for:

  • Functions that aren't passed as props (no child to save)
  • Functions passed to components that aren't wrapped in React.memo

The rule: profile first, optimize second. React DevTools has a Profiler tab that shows which components are slow and why. Don't add useMemo everywhere preemptively — it makes code harder to read and may not help.

Profiling in React DevTools

  1. Install React DevTools browser extension
  2. Open DevTools → Profiler tab
  3. Click "Record", interact with your app, click "Stop"
  4. See which components rendered and how long they took

Look for components rendering more than expected, or renders taking more than a few milliseconds. Those are your actual bottlenecks.

Next lesson: useRef and forwarding refs — accessing DOM nodes and storing mutable values.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!