L2: Refactor HomePage with useFetch
Replace manual fetch logic with the useFetch custom hook
Time to put our custom hook to work! We'll replace all the manual fetch logic in HomePage with a single line using useFetch.
What You'll Learn
- Use custom hooks in components
- Replace useState + useEffect patterns
- Simplify component code
- Handle loading and error states
- Pass parameters to custom hooks
Current HomePage Code
Right now, HomePage has about 30 lines of fetch logic:
import { useState, useEffect } from 'react';
import api from '@/api';
export function HomePage() {
const [listings, setListings] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchListings = async () => {
try {
setIsLoading(true);
const response = await api.get('/listings', {
signal: controller.signal
});
setListings(response.data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchListings();
return () => controller.abort();
}, []);
// Filter state
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// Filter logic
const filteredListings = listings.filter(listing => {
// ... filtering code
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
return (
<div>
<ListingFilters
search={search}
onSearchChange={setSearch}
dates={dates}
onDatesChange={setDates}
guests={guests}
onGuestsChange={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}All those highlighted lines can be replaced with useFetch!
Step-by-Step Refactoring
Import useFetch
import { useState } 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';We can remove useEffect and api imports - the hook handles those!
Replace fetch logic with useFetch
export function HomePage() {
// Replace all this:
// const [listings, setListings] = useState([]);
// const [isLoading, setIsLoading] = useState(true);
// const [error, setError] = useState(null);
// useEffect(() => { ... }, []);
// With this:
const { data: listings, isLoading, error } = useFetch('/listings');
// Filter state (unchanged)
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// ... rest of component
}One line replaces 30+ lines of boilerplate! 🎉
Note: We use data: listings to rename data to listings for clarity.
Handle null data case
Since useFetch initializes data as null, we need to handle that:
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 = (listings || []).filter(listing => {
const matchesSearch = listing.title
.toLowerCase()
.includes(search.toLowerCase());
const matchesGuests = listing.maxGuests >= guests;
// Date filtering logic...
return matchesSearch && matchesGuests;
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
return (
<div>
<ListingFilters
search={search}
onSearchChange={setSearch}
dates={dates}
onDatesChange={setDates}
guests={guests}
onGuestsChange={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}(listings || []) ensures we have an empty array if listings is null.
Complete Refactored Code
import { useState } 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() {
// Data fetching with custom hook
const { data: listings, isLoading, error } = useFetch('/listings');
// Filter state
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// Filter listings
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;
});
// Loading and error states
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
// Render
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 isAvailable(listing, from, to) {
// Date availability logic
return true; // Simplified for example
}Code Comparison
import { useState, useEffect } from 'react';
import api from '@/api';
export function HomePage() {
// 3 state variables
const [listings, setListings] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// 30+ lines of fetch logic
useEffect(() => {
const controller = new AbortController();
const fetchListings = async () => {
try {
setIsLoading(true);
const response = await api.get('/listings', {
signal: controller.signal
});
setListings(response.data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchListings();
return () => controller.abort();
}, []);
// ... rest
}Line count: ~50 lines (including fetch logic)
import { useState } from 'react';
import { useFetch } from '@/hooks/useFetch';
export function HomePage() {
// 1 line replaces 30+
const { data: listings, isLoading, error } = useFetch('/listings');
// ... rest
}Line count: ~25 lines (50% reduction!)
Removed:
- ❌ Manual state management
- ❌ useEffect boilerplate
- ❌ Fetch logic
- ❌ Error handling
- ❌ AbortController setup
Kept:
- ✅ Same functionality
- ✅ Same error handling
- ✅ Same loading states
- ✅ Cleaner code
Benefits
Code Reduction:
- Before: ~50 lines
- After: ~25 lines
- Savings: 50% fewer lines
Other benefits:
- Easier to read - Intent is clear
- Less error-prone - No manual state management
- Consistent - Same pattern everywhere
- Maintainable - Bug fixes in one place
- Testable - Hook can be tested separately
Testing the Refactored Code
Open your app and verify:
- Loading state - Should see spinner initially
- Data loads - Listings appear after fetch
- Filters work - Search, dates, guests still function
- No errors - Check browser console
Everything should work exactly the same, but with cleaner code!
Common Issues
What Changed?
Removed:
import { useEffect } from 'react';
import api from '@/api';
const [listings, setListings] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 25+ lines of fetch logic
}, []);Added:
import { useFetch } from '@/hooks/useFetch';
const { data: listings, isLoading, error } = useFetch('/listings');Result: Same functionality, 50% less code! 🎉
What's Next?
In Lesson 3, we'll refactor ListingDetailsPage to also use useFetch. We'll see even more benefits when using the hook with dynamic URLs! 🚀
Summary
- ✅ Replaced 30+ lines with 1 line
- ✅ Removed manual state management
- ✅ Removed useEffect boilerplate
- ✅ Kept all functionality
- ✅ Code is cleaner and easier to read
- ✅ HomePage is now 50% shorter
Key concept: Custom hooks make complex logic reusable and keep components focused on rendering!