Code To Learn logo

Code To Learn

M5: Hooks & Performance

L3: Refactor ListingDetailsPage

Use useFetch with dynamic URLs and route parameters

Let's apply useFetch to another component - this time with a dynamic URL that changes based on route parameters!

What You'll Learn

  • Use useFetch with dynamic URLs
  • Combine useParams with custom hooks
  • Handle URL parameter changes
  • Refetch data when params change
  • Simplify details page logic

Current ListingDetailsPage

Right now, ListingDetailsPage has similar fetch logic to what HomePage had:

src/pages/ListingDetailsPage.jsx
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import api from '@/api';

export function ListingDetailsPage() {
  const { id } = useParams();
  
  const [listing, setListing] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    const fetchListing = async () => {
      try {
        setIsLoading(true);
        const response = await api.get(`/listings/${id}`, {
          signal: controller.signal
        });
        setListing(response.data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchListing();
    return () => controller.abort();
  }, [id]);
  
  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  if (!listing) return <div>Listing not found</div>;
  
  return <ListingDetailsCard listing={listing} />;
}

Notice the URL uses the id from useParams - this is a dynamic URL!

Dynamic URLs with useFetch

The beauty of useFetch is that it automatically refetches when the URL changes:

// When id changes, useFetch automatically refetches!
const { data } = useFetch(`/listings/${id}`);

This works because:

  1. useFetch includes url in the dependency array
  2. When id changes, the URL changes
  3. useEffect runs again
  4. New data is fetched automatically

Key insight: Custom hooks can use values from the parent component (like id from useParams). When those values change, the hook automatically responds!

Step-by-Step Refactoring

Import useFetch

src/pages/ListingDetailsPage.jsx
import { useParams } from 'react-router-dom';
import { useFetch } from '@/hooks/useFetch';
import { ListingDetailsCard } from '@/components/ListingDetailsCard';
import { Spinner } from '@/components/ui/Spinner';
import { ErrorMessage } from '@/components/ui/ErrorMessage';

Remove useState, useEffect, and api imports.

Replace fetch logic

src/pages/ListingDetailsPage.jsx
export function ListingDetailsPage() {
  const { id } = useParams();
  
  // Replace all the fetch logic with this:
  const { data: listing, isLoading, error } = useFetch(`/listings/${id}`);
  
  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  if (!listing) return <div>Listing not found</div>;
  
  return <ListingDetailsCard listing={listing} />;
}

One line replaces 30+ lines again! The URL includes the dynamic id parameter.

Test URL changes

When you navigate between different listings, useFetch automatically:

  1. Detects URL changed (id changed)
  2. Cancels previous request (AbortController)
  3. Fetches new listing data
  4. Updates the UI

Try clicking between different listings - you'll see:

  • Loading spinner appears
  • New listing loads
  • Old request is canceled

All handled automatically! 🎉

Complete Refactored Code

src/pages/ListingDetailsPage.jsx
import { useParams } from 'react-router-dom';
import { useFetch } from '@/hooks/useFetch';
import { ListingDetailsCard } from '@/components/ListingDetailsCard';
import { Spinner } from '@/components/ui/Spinner';
import { ErrorMessage } from '@/components/ui/ErrorMessage';

export function ListingDetailsPage() {
  const { id } = useParams();
  const { data: listing, isLoading, error } = useFetch(`/listings/${id}`);
  
  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>
    );
  }
  
  if (!listing) {
    return (
      <div className="container mx-auto px-4 py-8">
        <p className="text-center text-gray-600">
          Listing not found
        </p>
      </div>
    );
  }
  
  return (
    <div className="container mx-auto px-4 py-8">
      <ListingDetailsCard listing={listing} />
    </div>
  );
}

From ~60 lines to ~35 lines - almost 40% reduction! 🚀

Code Comparison

import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import api from '@/api';

export function ListingDetailsPage() {
  const { id } = useParams();
  
  const [listing, setListing] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    const fetchListing = async () => {
      try {
        setIsLoading(true);
        const response = await api.get(`/listings/${id}`, {
          signal: controller.signal
        });
        setListing(response.data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchListing();
    return () => controller.abort();
  }, [id]);
  
  // ... render logic
}

Complexity:

  • Manual state management
  • AbortController setup
  • Error handling
  • Cleanup function
  • Dependency array
import { useParams } from 'react-router-dom';
import { useFetch } from '@/hooks/useFetch';

export function ListingDetailsPage() {
  const { id } = useParams();
  const { data: listing, isLoading, error } = useFetch(`/listings/${id}`);
  
  // ... render logic
}

Simplicity:

  • ✅ No manual state
  • ✅ No AbortController
  • ✅ No error handling
  • ✅ No cleanup
  • ✅ No dependency array

Everything is handled by useFetch!

How It Works

Let's understand the flow:

Benefits Summary

AspectBeforeAfter
Lines of code~60~35
State variables3 manual0 manual
useEffect hooks1 complex0
AbortControllerManual setupAutomatic
Error handlingManual try/catchAutomatic
Refetch logicManual in useEffectAutomatic
CleanupManual returnAutomatic

Real-World Usage Pattern

This pattern works for any dynamic URL:

// Different components, same pattern
function UserProfile() {
  const { userId } = useParams();
  const { data: user } = useFetch(`/users/${userId}`);
  return <Profile user={user} />;
}

function BlogPost() {
  const { slug } = useParams();
  const { data: post } = useFetch(`/posts/${slug}`);
  return <Article post={post} />;
}

function ProductDetails() {
  const { productId } = useParams();
  const { data: product } = useFetch(`/products/${productId}`);
  return <ProductCard product={product} />;
}

Same pattern everywhere - consistent and maintainable! ✨

Testing Your Changes

  1. Navigate to a listing - Should load correctly
  2. Click another listing - Should fetch new data
  3. Check browser console - No errors or warnings
  4. Navigate back and forth - Should work smoothly
  5. Check Network tab - Should see requests being canceled when URL changes

Common Issues

What Changed?

Removed:

import { useState, useEffect } from 'react';
import api from '@/api';

const [listing, setListing] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  // 25+ lines
}, [id]);

Added:

import { useFetch } from '@/hooks/useFetch';

const { data: listing, isLoading, error } = useFetch(`/listings/${id}`);

Gained:

  • ✅ 40% less code
  • ✅ Automatic refetch on URL change
  • ✅ Automatic cleanup
  • ✅ Consistent error handling
  • ✅ Same loading patterns

What's Next?

In Lesson 4, we'll learn about useMemo - a hook that prevents expensive calculations from running on every render. We'll optimize our listing filtering logic! 🚀

Summary

  • ✅ Refactored ListingDetailsPage with useFetch
  • ✅ Reduced code by 40%
  • ✅ Automatic refetch when URL changes
  • ✅ Same pattern as HomePage
  • ✅ Handles dynamic URLs seamlessly
  • ✅ Proper cleanup on unmount

Key concept: Custom hooks work perfectly with React Router - they automatically respond to URL parameter changes!