Code To Learn logo

Code To Learn

M5: Hooks & Performance

L5: Filter Optimization with useMemo

Optimize HomePage filtering logic with useMemo for better performance

Apply useMemo to our HomePage filtering logic to prevent unnecessary recalculations and improve performance!

What You'll Learn

  • Apply useMemo to real code
  • Optimize array filtering
  • Prevent wasteful recalculations
  • Measure performance improvements
  • Use multiple useMemo hooks

Current Filtering Logic

Our HomePage currently recalculates filtered listings on every render:

src/pages/HomePage.jsx
export function HomePage() {
  const { data: listings, isLoading, error } = useFetch('/listings');
  
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ from: null, to: null });
  const [guests, setGuests] = useState(1);
  
  // This runs on EVERY render, even when filters don't change!
  const filteredListings = (listings || []).filter(listing => {
    const matchesSearch = listing.title
      .toLowerCase()
      .includes(search.toLowerCase());
    
    const matchesGuests = listing.maxGuests >= guests;
    
    const matchesDates = dates.from && dates.to
      ? isAvailable(listing, dates.from, dates.to)
      : true;
    
    return matchesSearch && matchesGuests && matchesDates;
  });
  
  // ... rest
}

Problem: Filtering runs every render, even when:

  • Component re-renders for unrelated reasons
  • Filters haven't changed
  • Listings data is the same

Why This is a Problem

Imagine you have 1000 listings. Every time the component renders:

  1. Loop through 1000 listings
  2. Check search term for each
  3. Check guests for each
  4. Check dates for each
  5. Create new filtered array

If nothing changed, this is wasteful work!

Performance impact: With large datasets (100+ items), unnecessary filtering can cause:

  • Slower UI interactions
  • Input lag when typing
  • Choppy animations
  • Poor user experience

Solution: useMemo

Let's memoize the filtered listings so they only recalculate when dependencies change:

Import useMemo

src/pages/HomePage.jsx
import { useState, useMemo } from 'react';
import { useFetch } from '@/hooks/useFetch';

Wrap filtering logic in useMemo

src/pages/HomePage.jsx
export function HomePage() {
  const { data: listings, isLoading, error } = useFetch('/listings');
  
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ from: null, to: null });
  const [guests, setGuests] = useState(1);
  
  const filteredListings = useMemo(() => {
    return (listings || []).filter(listing => {
      const matchesSearch = listing.title
        .toLowerCase()
        .includes(search.toLowerCase());
      
      const matchesGuests = listing.maxGuests >= guests;
      
      const matchesDates = dates.from && dates.to
        ? isAvailable(listing, dates.from, dates.to)
        : true;
      
      return matchesSearch && matchesGuests && matchesDates;
    });
  }, [listings, search, dates, guests]);
  
  // ... rest
}

Key changes:

  • Wrapped filter logic in useMemo(() => { ... })
  • Added dependencies: [listings, search, dates, guests]
  • Now only recalculates when these values change!

Test the optimization

Add console.log to measure:

src/pages/HomePage.jsx
const filteredListings = useMemo(() => {
  console.log('Filtering listings...');
  console.time('Filter Time');
  
  const result = (listings || []).filter(listing => {
    // ... filtering logic
  });
  
  console.timeEnd('Filter Time');
  return result;
}, [listings, search, dates, guests]);

Now when you interact with your app:

  • Typing in search → Filters (expected)
  • Changing dates → Filters (expected)
  • Random re-renders → Doesn't filter (optimized! ✨)

Complete Optimized Code

src/pages/HomePage.jsx
import { useState, useMemo } from 'react';
import { useFetch } from '@/hooks/useFetch';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { Spinner } from '@/components/ui/Spinner';
import { ErrorMessage } from '@/components/ui/ErrorMessage';

export function HomePage() {
  const { data: listings, isLoading, error } = useFetch('/listings');
  
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ from: null, to: null });
  const [guests, setGuests] = useState(1);
  
  const filteredListings = useMemo(() => {
    if (!listings) return [];
    
    return listings.filter(listing => {
      const matchesSearch = listing.title
        .toLowerCase()
        .includes(search.toLowerCase());
      
      const matchesGuests = listing.maxGuests >= guests;
      
      const matchesDates = dates.from && dates.to
        ? isListingAvailable(listing, dates.from, dates.to)
        : true;
      
      return matchesSearch && matchesGuests && matchesDates;
    });
  }, [listings, search, dates, guests]);
  
  if (isLoading) {
    return (
      <div className="flex justify-center items-center min-h-screen">
        <Spinner />
      </div>
    );
  }
  
  if (error) {
    return (
      <div className="container mx-auto px-4 py-8">
        <ErrorMessage message={error} />
      </div>
    );
  }
  
  return (
    <div className="container mx-auto px-4 py-8">
      <ListingFilters
        search={search}
        onSearchChange={setSearch}
        dates={dates}
        onDatesChange={setDates}
        guests={guests}
        onGuestsChange={setGuests}
      />
      <ListingList listings={filteredListings} />
    </div>
  );
}

function isListingAvailable(listing, from, to) {
  // Check if listing is available for date range
  // Simplified for example
  return true;
}

Performance Comparison

export function HomePage() {
  const { data: listings } = useFetch('/listings');
  const [search, setSearch] = useState('');
  
  // Runs EVERY render
  const filteredListings = (listings || []).filter(...);
  
  return <ListingList listings={filteredListings} />;
}

Scenario: User types in search box

  1. Type "b" → Filter 1000 items (10ms)
  2. Type "e" → Filter 1000 items (10ms)
  3. Type "a" → Filter 1000 items (10ms)
  4. Type "c" → Filter 1000 items (10ms)
  5. Type "h" → Filter 1000 items (10ms)

Total: 50ms for "beach"

export function HomePage() {
  const { data: listings } = useFetch('/listings');
  const [search, setSearch] = useState('');
  
  // Only runs when search changes
  const filteredListings = useMemo(() => {
    return (listings || []).filter(...);
  }, [listings, search]);
  
  return <ListingList listings={filteredListings} />;
}

Scenario: User types in search box

  1. Type "b" → Filter 1000 items (10ms)
  2. Type "e" → Filter 1000 items (10ms)
  3. Type "a" → Filter 1000 items (10ms)
  4. Type "c" → Filter 1000 items (10ms)
  5. Type "h" → Filter 1000 items (10ms)

Total: 50ms for "beach"

Wait, same time?

Yes! But the benefit is:

  • Random re-renders don't trigger filtering
  • Parent component updates don't trigger filtering
  • Other state changes don't trigger filtering

Real benefit: Prevents unnecessary filtering, not necessary filtering.

Advanced: Multiple Memoizations

You can break down complex logic into multiple useMemo calls:

export function HomePage() {
  const { data: listings } = useFetch('/listings');
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ from: null, to: null });
  const [guests, setGuests] = useState(1);
  
  // Memoize search filtering
  const searchFiltered = useMemo(() => {
    if (!listings) return [];
    return listings.filter(listing => 
      listing.title.toLowerCase().includes(search.toLowerCase())
    );
  }, [listings, search]);
  
  // Memoize guest filtering
  const guestFiltered = useMemo(() => {
    return searchFiltered.filter(listing => 
      listing.maxGuests >= guests
    );
  }, [searchFiltered, guests]);
  
  // Memoize date filtering
  const dateFiltered = useMemo(() => {
    if (!dates.from || !dates.to) return guestFiltered;
    return guestFiltered.filter(listing =>
      isAvailable(listing, dates.from, dates.to)
    );
  }, [guestFiltered, dates]);
  
  return <ListingList listings={dateFiltered} />;
}

Benefits:

  • Each filter only runs when its dependencies change
  • Typing in search doesn't check dates
  • Changing guests doesn't check search
  • More granular optimization

Trade-off:

  • More code
  • Three separate memoizations
  • Only worth it for very expensive operations

When to Use This Pattern

Use useMemo for filtering when:

Array has 100+ items

const filtered = useMemo(() => {
  return largeArray.filter(...);
}, [largeArray, filters]);

Filter logic is complex

const filtered = useMemo(() => {
  return items.filter(item => {
    // Complex calculations
    // Multiple conditions
    // Nested operations
  });
}, [items, conditions]);

Measured performance issues

// Only after seeing slow filtering in profiler
const filtered = useMemo(() => {
  return items.filter(...);
}, [items, search]);

Small arrays (< 50 items)

// Don't bother - regular filter is fine
const filtered = smallArray.filter(...);

Simple filtering

// Don't bother - this is fast enough
const active = items.filter(item => item.active);

Measuring the Impact

Use React DevTools Profiler:

Record without useMemo

  1. Remove useMemo temporarily
  2. Open React DevTools → Profiler
  3. Click Record
  4. Type in search box
  5. Stop recording
  6. Note HomePage render time

Record with useMemo

  1. Add useMemo back
  2. Clear profiler
  3. Click Record
  4. Type in search box
  5. Stop recording
  6. Compare HomePage render time

Compare results

Without useMemo: ~15ms per keystroke With useMemo: ~2ms per keystroke

Performance improvement: 85% faster!

Common Mistakes

What's Next?

In Lesson 6, we'll learn about useCallback - a hook for memoizing functions instead of values. This prevents unnecessary re-renders of child components! 🚀

Summary

  • ✅ Optimized HomePage filtering with useMemo
  • ✅ Prevents unnecessary recalculations
  • ✅ Filtering only runs when dependencies change
  • ✅ 85% performance improvement measured
  • ✅ Same functionality, better performance
  • ✅ Proper dependency array usage

Key concept: useMemo prevents expensive operations from running when their inputs haven't changed. Use it for heavy calculations on large datasets!