L3: Fetching Listings Data
Use useEffect to fetch listings from the API and replace static data
What You'll Learn
Now it's time to make your app dynamic! In this lesson, you'll:
- Replace static listings with API-fetched data
- Use
useEffectto fetch data when the component mounts - Handle asynchronous operations in React
- Update state with fetched data
- See the dependency array in action
Current State
Right now, your HomePage looks something like this:
import { useState } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
export function HomePage() {
// Static data - never changes!
const [listings, setListings] = useState([
{
id: 1,
title: "Beachfront Paradise",
location: "Malibu, CA",
pricePerNight: 450,
// ... more fields
},
// ... more listings
]);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ checkIn: null, checkOut: null });
const [guests, setGuests] = useState(1);
// Filter logic...
return (
<div>
<ListingFilters
search={search}
onSearchChange={setSearch}
dates={dates}
onDatesChange={setDates}
guests={guests}
onGuestsChange={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}Problems:
- ❌ Data is hardcoded - can't update from a real backend
- ❌ Always shows the same listings
- ❌ No way to add/remove listings dynamically
- ❌ Doesn't reflect real-world usage
Solution: Fetch data from the API!
The Plan
We'll transform the HomePage to:
- Start with empty array for listings
- Use useEffect to fetch data when component mounts
- Update state with the fetched data
- React re-renders with the new data
Step 1: Update Initial State
First, change the initial state from hardcoded data to an empty array:
import { useState } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
export function HomePage() {
// Start with empty array - data will be fetched!
const [listings, setListings] = useState([]);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ checkIn: null, checkOut: null });
const [guests, setGuests] = useState(1);
// ... rest of component
}What changes:
- Before:
useState([{...}, {...}])- hardcoded data - After:
useState([])- empty array
Important: Starting with an empty array means the component will initially show "no listings" until the fetch completes. We'll add a loading state in the next lesson!
Step 2: Import the API Function
Add the import at the top of your file:
import { useState, useEffect } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { getAllListings } from '@/api'; What we added:
useEffectfrom ReactgetAllListingsfrom our API module
Step 3: Add useEffect to Fetch Data
Now add the effect that fetches listings:
export function HomePage() {
const [listings, setListings] = useState([]);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ checkIn: null, checkOut: null });
const [guests, setGuests] = useState(1);
// Fetch listings when component mounts
useEffect(() => {
const fetchListings = async () => {
const data = await getAllListings();
setListings(data);
};
fetchListings();
}, []); // Empty array = run once on mount
// Filter listings based on search, dates, guests
const filteredListings = listings.filter(listing => {
// ... existing filter logic
});
return (
<div>
<ListingFilters
search={search}
onSearchChange={setSearch}
dates={dates}
onDatesChange={setDates}
guests={guests}
onGuestsChange={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}What's happening:
useEffect(() => {...}, [])- Effect runs once after component mountsconst fetchListings = async () => {...}- Create async function inside effectawait getAllListings()- Fetch data from API (waits for response)setListings(data)- Update state with fetched datafetchListings()- Call the async function[]- Empty dependency array = run once
Step 4: Test It Out
Save your file and check the browser. You should see:
- Brief moment: No listings (empty array)
- ~1.5 second delay: API is "fetching" (simulated)
- Data appears: Listings render with fetched data
Open DevTools Console:
You should see:
Effect ran!
Fetched listings: (6) [{...}, {...}, ...]Understanding the Code
Let's break down the useEffect pattern:
Why Create a Separate Async Function?
useEffect(() => {
const fetchData = async () => {
const data = await apiCall();
setState(data);
};
fetchData();
}, []);Why not make the effect callback async?
useEffect(async () => {
const data = await apiCall();
setState(data);
}, []);Problem: useEffect expects the callback to return either:
undefined(most common)- A cleanup function
But async functions always return a Promise, which confuses React.
Solution: Create an async function inside the effect, then call it immediately.
Why Empty Dependency Array []?
// Run once on mount
useEffect(() => {
fetchListings();
}, []); // Empty array
// Run on every render (DON'T DO THIS!)
useEffect(() => {
fetchListings();
}); // No array - causes infinite loops!
// Run when 'search' changes
useEffect(() => {
fetchListings(search);
}, [search]); // Re-fetch when search changesFor initial data fetch:
- Empty array
[]means "run once when component mounts" - Perfect for loading initial data
- Won't re-run on every render (would be wasteful!)
Common Mistake: Forgetting the dependency array causes infinite loops:
- Component renders
- Effect runs, fetches data
setListings(data)updates state- State change triggers re-render
- Effect runs again (because no deps array!)
- Repeat forever ♾️
Why Not Async useEffect Directly?
What you might want to write:
useEffect(async () => {
const data = await getAllListings();
setListings(data);
}, []);Error:
Warning: useEffect must not return anything besides a function,
which is used for clean-up. Why it fails:
// Async functions always return a Promise
const myAsync = async () => {
return "value";
};
const result = myAsync();
console.log(result); // Promise { <pending> }useEffect expects:
// Option 1: Return nothing
useEffect(() => {
doSomething();
}, []);
// Option 2: Return cleanup function
useEffect(() => {
doSomething();
return () => {
cleanup();
};
}, []);Solution - Wrapper Function Pattern:
useEffect(() => {
// Define async function
const loadData = async () => {
const data = await apiCall();
setState(data);
};
// Call it immediately
loadData();
// Optionally return cleanup
return () => {
// cleanup code
};
}, []);Execution Flow
Timeline of what happens:
1. Component mounts
↓
2. Initial render with listings = []
↓
3. Browser paints empty state
↓
4. useEffect runs (after paint)
↓
5. fetchListings() called
↓
6. await getAllListings() - Network request starts
↓
7. [1.5 seconds pass...]
↓
8. API returns data
↓
9. setListings(data) - State updated
↓
10. Re-render triggered
↓
11. Component renders with listings = [...data]
↓
12. Browser paints listingsKey Points:
- useEffect runs after the first render
- UI is not blocked during fetch
- State update triggers a re-render
- Second render shows the data
Complete Updated Component
Here's your full HomePage with data fetching:
import { useState, useEffect } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { getAllListings } from '@/api';
export function HomePage() {
// State
const [listings, setListings] = useState([]);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ checkIn: null, checkOut: null });
const [guests, setGuests] = useState(1);
// Fetch listings on mount
useEffect(() => {
const fetchListings = async () => {
try {
const data = await getAllListings();
setListings(data);
console.log('Fetched listings:', data);
} catch (error) {
console.error('Failed to fetch listings:', error);
}
};
fetchListings();
}, []); // Run once on mount
// Filter listings
const filteredListings = listings.filter(listing => {
// Search filter
const matchesSearch =
search === '' ||
listing.title.toLowerCase().includes(search.toLowerCase()) ||
listing.location.toLowerCase().includes(search.toLowerCase());
// Guests filter
const matchesGuests = listing.maxGuests >= guests;
// Date filter (simplified - would be more complex in real app)
const matchesDates = true; // Assume dates match for now
return matchesSearch && matchesGuests && matchesDates;
});
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={filteredListings} />
</div>
</div>
</div>
);
}Common Patterns and Variations
What You've Accomplished
Your app is now dynamic!
Before:
- ❌ Static hardcoded data
- ❌ No way to update listings
- ❌ Doesn't reflect real-world usage
After:
- ✅ Data fetched from API
- ✅ Updates when component mounts
- ✅ Ready for real backend integration
- ✅ Follows React best practices
What you learned:
- Using useEffect for data fetching
- Async function wrapper pattern
- Dependency array for mount-only effects
- Updating state with fetched data
Current Issues
Your app works, but there are problems:
- No loading state - Brief flash of empty listings
- No error handling - Silent failures
- No feedback - User doesn't know data is loading
- Potential bugs - Race conditions if user navigates away
We'll fix all of these in the next lessons!
Key Takeaways
The Data Fetching Pattern:
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await apiCall();
setData(result);
};
fetchData();
}, []); // Empty array = fetch onceRemember:
- ✅ Use empty array
[]for initial data fetch - ✅ Create async function inside effect
- ✅ Call the async function immediately
- ✅ Update state with fetched data
- ❌ Don't make effect callback async directly
- ❌ Don't forget dependency array (infinite loops!)
What's Next?
In the next lesson, you'll add loading states:
- 🔄 Show spinner while data loads
- ✨ Create a
Spinnercomponent - 📊 Track loading state with
isLoading - 💫 Provide better user experience
Your users will know when data is loading! 🚀