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:
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:
useFetchincludesurlin the dependency array- When
idchanges, the URL changes useEffectruns again- 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
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
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:
- Detects URL changed (id changed)
- Cancels previous request (AbortController)
- Fetches new listing data
- 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
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
| Aspect | Before | After |
|---|---|---|
| Lines of code | ~60 | ~35 |
| State variables | 3 manual | 0 manual |
| useEffect hooks | 1 complex | 0 |
| AbortController | Manual setup | Automatic |
| Error handling | Manual try/catch | Automatic |
| Refetch logic | Manual in useEffect | Automatic |
| Cleanup | Manual return | Automatic |
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
- Navigate to a listing - Should load correctly
- Click another listing - Should fetch new data
- Check browser console - No errors or warnings
- Navigate back and forth - Should work smoothly
- 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!