Code To Learn logo

Code To Learn

M3: Effects & Data

L7: Refactoring with Custom Hooks

Extract reusable logic into custom hooks for cleaner, more maintainable code

What You'll Learn

Your HomePage works great, but it's getting crowded with state management, effects, and logic. Time to clean it up! In this lesson, you'll:

  • Understand custom hooks and why they matter
  • Extract data fetching logic into useFetchListings hook
  • Create reusable hooks for common patterns
  • Separate concerns for better maintainability
  • Follow React best practices for code organization

The Problem: Component Complexity

Look at your HomePage now:

src/pages/HomePage.jsx (Getting Crowded)
export function HomePage() {
  // State (7 variables!)
  const [listings, setListings] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ checkIn: null, checkOut: null });
  const [guests, setGuests] = useState(1);

  // Data fetching logic (20+ lines)
  useEffect(() => {
    const controller = new AbortController();
    const fetchListings = async () => {
      // ... lots of code ...
    };
    fetchListings();
    return () => controller.abort();
  }, []);

  // Filtering logic (10+ lines)
  const filteredListings = listings.filter(listing => {
    // ... filtering code ...
  });

  // Conditional rendering (60+ lines)
  if (error) return <ErrorMessage />;
  if (isLoading) return <Spinner />;
  return <div>{/* main UI */}</div>;
}

Problems:

  • 😵 Too much code in one component (100+ lines)
  • 🔄 Data fetching logic not reusable
  • 🐛 Hard to test individual pieces
  • 📚 Difficult to understand at a glance

Solution: Custom Hooks!

What Are Custom Hooks?

Custom hooks are JavaScript functions that:

  • Start with "use" (e.g., useFetchListings)
  • Can call other hooks (useState, useEffect, etc.)
  • Return values and/or functions
  • Encapsulate reusable logic
Custom Hook Example
function useFetchListings() {
  const [listings, setListings] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Fetch logic here
  }, []);

  return { listings, isLoading, error };
}

// Use it in component
function HomePage() {
  const { listings, isLoading, error } = useFetchListings();
  // Component is now much simpler!
}

Key Concept: Custom hooks let you extract component logic into reusable functions. Think of them as "helper functions for React features."

Step 1: Create useFetchListings Hook

Create Hooks Directory

Terminal
mkdir -p src/hooks

Project structure:

useFetchListings.js
HomePage.jsx
index.js

Extract Data Fetching Logic

src/hooks/useFetchListings.js
import { useState, useEffect } from 'react';
import { getAllListings } from '@/api';

/**
 * Custom hook for fetching listings with loading and error states
 * @returns {Object} - { listings, isLoading, error, refetch }
 */
export function useFetchListings() {
  const [listings, setListings] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchListings = async (signal) => {
    setIsLoading(true);
    setError(null);
    
    try {
      const data = await getAllListings({ signal });
      setListings(data);
      console.log('Fetched listings:', data);
    } catch (err) {
      if (err.message !== 'Request cancelled') {
        console.error('Failed to fetch listings:', err);
        setError(err.message || 'Failed to load listings');
      }
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    const controller = new AbortController();
    fetchListings(controller.signal);

    return () => {
      controller.abort();
    };
  }, []);

  // Expose refetch function for manual reload
  const refetch = () => {
    const controller = new AbortController();
    fetchListings(controller.signal);
  };

  return { 
    listings, 
    isLoading, 
    error,
    refetch // Bonus: manual refetch capability
  };
}

What this hook provides:

  • listings - The fetched data
  • isLoading - Loading state boolean
  • error - Error message or null
  • refetch - Function to manually reload data

Update HomePage to Use Hook

src/pages/HomePage.jsx (Much Cleaner!)
import { useState } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { Spinner } from '@/components/Spinner';
import { ErrorMessage } from '@/components/ErrorMessage';
import { useFetchListings } from '@/hooks/useFetchListings'; 

export function HomePage() {
  // Custom hook replaces all this state and useEffect!
  const { listings, isLoading, error, refetch } = useFetchListings(); 

  // Filter state
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ checkIn: null, checkOut: null });
  const [guests, setGuests] = useState(1);

  // Filter listings
  const filteredListings = listings.filter(listing => {
    const matchesSearch = 
      search === '' ||
      listing.title.toLowerCase().includes(search.toLowerCase()) ||
      listing.location.toLowerCase().includes(search.toLowerCase());

    const matchesGuests = listing.maxGuests >= guests;
    const matchesDates = true;

    return matchesSearch && matchesGuests && matchesDates;
  });

  // Error state
  if (error) {
    return (
      <div className="min-h-screen bg-gray-50">
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">
            Find Your Perfect Stay
          </h1>
          <ErrorMessage 
            title="Unable to Load Listings"
            message={error}
            onRetry={refetch} 
          />
        </div>
      </div>
    );
  }

  // Loading state
  if (isLoading) {
    return (
      <div className="min-h-screen bg-gray-50">
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">
            Find Your Perfect Stay
          </h1>
          <Spinner 
            size="large"
            message="Loading amazing stays..."
          />
        </div>
      </div>
    );
  }

  // Main content
  return (
    <div className="min-h-screen bg-gray-50">
      <div className="container mx-auto px-4 py-8">
        <h1 className="text-3xl font-bold mb-8">Find Your Perfect Stay</h1>
        
        <ListingFilters 
          search={search}
          onSearchChange={setSearch}
          dates={dates}
          onDatesChange={setDates}
          guests={guests}
          onGuestsChange={setGuests}
        />

        <div className="mt-8">
          <p className="text-gray-600 mb-4">
            Showing {filteredListings.length} of {listings.length} listings
          </p>
          
          <ListingList listings={listings} />
        </div>
      </div>
    </div>
  );
}

Benefits:

  • ✅ HomePage is 30% shorter
  • ✅ Data fetching logic is reusable
  • ✅ Easy to test the hook independently
  • ✅ Clear separation of concerns

Step 2: Create useFilteredListings Hook

Let's also extract the filtering logic:

src/hooks/useFilteredListings.js
/**
 * Custom hook for filtering listings
 * @param {Array} listings - Array of listings to filter
 * @param {Object} filters - Filter criteria
 * @returns {Array} - Filtered listings
 */
export function useFilteredListings(listings, filters) {
  const { search, guests, dates } = filters;

  return listings.filter(listing => {
    // Search filter
    const matchesSearch = 
      !search ||
      listing.title.toLowerCase().includes(search.toLowerCase()) ||
      listing.location.toLowerCase().includes(search.toLowerCase()) ||
      listing.description.toLowerCase().includes(search.toLowerCase());

    // Guests filter
    const matchesGuests = listing.maxGuests >= guests;

    // Date filter (simplified for now)
    const matchesDates = true; // Would check availability dates

    return matchesSearch && matchesGuests && matchesDates;
  });
}

Use in HomePage:

src/pages/HomePage.jsx
import { useFilteredListings } from '@/hooks/useFilteredListings';

export function HomePage() {
  const { listings, isLoading, error, refetch } = useFetchListings();
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ checkIn: null, checkOut: null });
  const [guests, setGuests] = useState(1);

  // One line instead of 15!
  const filteredListings = useFilteredListings(listings, { 
    search, 
    guests, 
    dates 
  }); 

  // ... rest of component
}

Custom Hook Patterns

Generic Data Fetching Hook

src/hooks/useFetch.js
import { useState, useEffect } from 'react';

export function useFetch(fetchFn, dependencies = []) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const loadData = async () => {
      setIsLoading(true);
      setError(null);

      try {
        const result = await fetchFn({ signal: controller.signal });
        setData(result);
      } catch (err) {
        if (err.message !== 'Request cancelled') {
          setError(err.message);
        }
      } finally {
        setIsLoading(false);
      }
    };

    loadData();

    return () => controller.abort();
  }, dependencies);

  return { data, isLoading, error };
}

Usage:

const { data: listings } = useFetch(getAllListings);
const { data: user } = useFetch(() => getUserById(userId), [userId]);

Local Storage Hook

src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';

export function useLocalStorage(key, initialValue) {
  // Get initial value from localStorage or use default
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  // Update localStorage when value changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

Usage:

// Persists across page reloads!
const [favorites, setFavorites] = useLocalStorage('favorites', []);
const [theme, setTheme] = useLocalStorage('theme', 'light');

Window Size Hook

src/hooks/useWindowSize.js
import { useState, useEffect } from 'react';

export function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return size;
}

Usage:

const { width, height } = useWindowSize();
const isMobile = width < 768;

return (
  <div>
    {isMobile ? <MobileNav /> : <DesktopNav />}
  </div>
);

Toggle Hook

src/hooks/useToggle.js
import { useState } from 'react';

export function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = () => setValue(v => !v);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);

  return [value, toggle, setTrue, setFalse];
}

Usage:

const [isOpen, toggle, open, close] = useToggle(false);

return (
  <>
    <button onClick={toggle}>Toggle Modal</button>
    <Modal isOpen={isOpen} onClose={close}>
      Content
    </Modal>
  </>
);

Custom Hook Best Practices

Testing Custom Hooks

Custom hooks are easier to test than components:

__tests__/useFetchListings.test.js
import { renderHook, waitFor } from '@testing-library/react';
import { useFetchListings } from '../hooks/useFetchListings';

describe('useFetchListings', () => {
  it('should fetch listings on mount', async () => {
    const { result } = renderHook(() => useFetchListings());

    // Initial state
    expect(result.current.isLoading).toBe(true);
    expect(result.current.listings).toEqual([]);

    // Wait for fetch to complete
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    // Check data loaded
    expect(result.current.listings.length).toBeGreaterThan(0);
    expect(result.current.error).toBeNull();
  });

  it('should handle errors', async () => {
    // Mock API to fail
    jest.spyOn(global, 'fetch').mockRejectedValue(new Error('Network error'));

    const { result } = renderHook(() => useFetchListings());

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.error).toBe('Network error');
    expect(result.current.listings).toEqual([]);
  });
});

Real-World Custom Hooks Library

Here are some battle-tested custom hooks from popular libraries:

React Use (react-use npm package):

  • useAsync - Async operation state
  • useDebounce - Debounced value
  • useInterval - Declarative intervals
  • useLocalStorage - Persistent state
  • useToggle - Boolean toggle
  • usePrevious - Previous value

Example:

npm install react-use
import { useDebounce, useLocalStorage } from 'react-use';

function SearchComponent() {
  const [search, setSearch] = useState('');
  const [favorites, setFavorites] = useLocalStorage('favorites', []);
  const debouncedSearch = useDebounce(search, 500);

  // debouncedSearch only updates 500ms after user stops typing
}

Key Takeaways

Custom hooks rock!

What you learned:

  • ✅ Custom hooks extract reusable logic
  • ✅ Start hook names with "use"
  • ✅ Can call other hooks inside custom hooks
  • ✅ Return objects for multiple values
  • ✅ Keep hooks focused on one responsibility
  • ✅ Much easier to test than components

When to create a custom hook:

  • 🔄 Logic is used in multiple components
  • 🧹 Component is getting too complex
  • 🧪 Want to test logic separately
  • 📦 Want to share logic across projects

Next: Build an image carousel component!

What's Next?

Your code is now clean and maintainable! In the next lesson, you'll apply your knowledge to build a real feature:

  • 🎠 Create an image carousel component
  • 🖼️ Handle multiple images per listing
  • ⬅️➡️ Add prev/next navigation
  • 🔄 Use useEffect for auto-play
  • ⏱️ Practice cleanup with timers

Let's build something visual! 🚀