Code To Learn logo

Code To Learn

M3: Effects & Data

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 useEffect to 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:

src/pages/HomePage.jsx (Current - Static Data)
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:

  1. Start with empty array for listings
  2. Use useEffect to fetch data when component mounts
  3. Update state with the fetched data
  4. React re-renders with the new data

Step 1: Update Initial State

First, change the initial state from hardcoded data to an empty array:

src/pages/HomePage.jsx
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:

src/pages/HomePage.jsx
import { useState, useEffect } from 'react'; 
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { getAllListings } from '@/api'; 

What we added:

  • useEffect from React
  • getAllListings from our API module

Step 3: Add useEffect to Fetch Data

Now add the effect that fetches listings:

src/pages/HomePage.jsx
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:

  1. useEffect(() => {...}, []) - Effect runs once after component mounts
  2. const fetchListings = async () => {...} - Create async function inside effect
  3. await getAllListings() - Fetch data from API (waits for response)
  4. setListings(data) - Update state with fetched data
  5. fetchListings() - Call the async function
  6. [] - Empty dependency array = run once

Step 4: Test It Out

Save your file and check the browser. You should see:

  1. Brief moment: No listings (empty array)
  2. ~1.5 second delay: API is "fetching" (simulated)
  3. 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?

Pattern Breakdown
useEffect(() => {
  const fetchData = async () => {
    const data = await apiCall();
    setState(data);
  };
  
  fetchData();
}, []);

Why not make the effect callback async?

❌ This doesn't work
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 []?

Dependency Array Comparison

// 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 changes

For 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:

  1. Component renders
  2. Effect runs, fetches data
  3. setListings(data) updates state
  4. State change triggers re-render
  5. Effect runs again (because no deps array!)
  6. Repeat forever ♾️

Why Not Async useEffect Directly?

What you might want to write:

❌ Invalid Syntax
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:

Understanding the Error
// Async functions always return a Promise
const myAsync = async () => {
  return "value";
};

const result = myAsync();
console.log(result); // Promise { <pending> }

useEffect expects:

Valid Return Values
// Option 1: Return nothing
useEffect(() => {
  doSomething();
}, []);

// Option 2: Return cleanup function
useEffect(() => {
  doSomething();
  
  return () => {
    cleanup();
  };
}, []);

Solution - Wrapper Function Pattern:

✅ Correct 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 listings

Key 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:

src/pages/HomePage.jsx (Complete)
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:

  1. No loading state - Brief flash of empty listings
  2. No error handling - Silent failures
  3. No feedback - User doesn't know data is loading
  4. Potential bugs - Race conditions if user navigates away

We'll fix all of these in the next lessons!

Key Takeaways

The Data Fetching Pattern:

Standard Pattern
const [data, setData] = useState([]);

useEffect(() => {
  const fetchData = async () => {
    const result = await apiCall();
    setData(result);
  };
  
  fetchData();
}, []); // Empty array = fetch once

Remember:

  • ✅ 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 Spinner component
  • 📊 Track loading state with isLoading
  • 💫 Provide better user experience

Your users will know when data is loading! 🚀