Code To Learn logo

Code To Learn

M4: Routes & Navigation

L8: Creating ListingDetailsCard Component

Extract listing display logic into a reusable component

Right now, ListingDetailsPage handles both data fetching AND display logic. Let's follow React best practices by extracting the display part into a separate component!

This makes our code cleaner, more reusable, and easier to test.

What You'll Learn

  • Extract display logic into a component
  • Design clear component props interfaces
  • Separate concerns (data vs presentation)
  • Create reusable UI components
  • Component composition patterns

Why Extract Components?

Current problem:

// ListingDetailsPage does EVERYTHING
function ListingDetailsPage() {
  // 1. Data fetching (useEffect, fetch, state)
  // 2. Loading/error handling
  // 3. Display logic (JSX)
  // = 200+ lines in one component!
}

Better approach:

// Page handles data, Card handles display
function ListingDetailsPage() {
  // Data fetching only
  const listing = useFetch(`/api/listings/${id}`);
  return <ListingDetailsCard listing={listing} />;
}

function ListingDetailsCard({ listing }) {
  // Display logic only
  return <div>{/* JSX */}</div>;
}

Benefits:

  • ✅ Single Responsibility Principle
  • ✅ Easier to test
  • ✅ Reusable across pages
  • ✅ Cleaner, more maintainable code

Step 1: Create the Component File

Create ListingDetailsCard.jsx

Create the new component file:

Terminal
touch src/components/ListingDetailsCard.jsx

Your components folder should have:

Router.jsx
ListingDetailsCard.jsx

Step 2: Build the Component

Add Component Structure

Create the card component:

src/components/ListingDetailsCard.jsx
export default function ListingDetailsCard({ listing }) {
  return (
    <div className="container mx-auto px-4 py-8">
      {/* Page Header */}
      <div className="mb-6">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">
          {listing.title}
        </h1>
        <div className="flex items-center gap-4 text-sm text-gray-600">
          <span>📍 {listing.location}</span>
          <span>⭐ {listing.rating}</span>
          <span>👥 {listing.reviews} reviews</span>
        </div>
      </div>

      {/* Image Placeholder */}
      <div className="bg-gray-200 rounded-lg h-96 mb-6 flex items-center justify-center">
        <p className="text-gray-500 text-lg">Image Gallery Coming Soon</p>
      </div>

      {/* Main Content Grid */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        {/* Left Column - Main Info */}
        <div className="lg:col-span-2 space-y-6">
          {/* Host Info */}
          <div className="border-b border-gray-200 pb-6">
            <h2 className="text-xl font-semibold mb-2">
              Entire place hosted by {listing.host?.name || 'Host'}
            </h2>
            <div className="text-gray-600">
              {listing.guests} guests · {listing.bedrooms} bedrooms · {listing.bathrooms} bathrooms
            </div>
          </div>

          {/* Description */}
          <div className="border-b border-gray-200 pb-6">
            <h2 className="text-xl font-semibold mb-3">About this place</h2>
            <p className="text-gray-600 leading-relaxed">
              {listing.description}
            </p>
          </div>

          {/* Amenities */}
          <div className="border-b border-gray-200 pb-6">
            <h2 className="text-xl font-semibold mb-3">What this place offers</h2>
            <div className="grid grid-cols-2 gap-4">
              {listing.amenities?.map((amenity, index) => (
                <div key={index} className="flex items-center gap-3">
                  <span>{amenity.icon}</span>
                  <span>{amenity.name}</span>
                </div>
              ))}
            </div>
          </div>
        </div>

        {/* Right Column - Booking Card */}
        <div className="lg:col-span-1">
          <div className="border border-gray-200 rounded-lg p-6 shadow-md sticky top-4">
            <div className="mb-4">
              <span className="text-2xl font-bold">${listing.price}</span>
              <span className="text-gray-600"> / night</span>
            </div>

            <div className="mb-4">
              <div className="text-sm text-gray-600 mb-2">
                ⭐ {listing.rating} · {listing.reviews} reviews
              </div>
            </div>

            <button className="w-full bg-pink-600 hover:bg-pink-700 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
              Check Availability
            </button>

            <p className="text-center text-sm text-gray-600 mt-4">
              You won't be charged yet
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}

What this component does:

  • Receives listing data as a prop
  • Displays all listing information
  • No data fetching (pure presentation)
  • Reusable in other pages

Step 3: Update ListingDetailsPage

Now simplify the page component:

Refactor to Use Card Component

Update ListingDetailsPage.jsx:

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

export default function ListingDetailsPage() {
  const { id } = useParams();
  const [listing, setListing] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    setError(null);

    const controller = new AbortController();

    fetch(`/api/listings/${id}`, {
      signal: controller.signal
    })
      .then(response => {
        if (!response.ok) {
          throw new Error('Listing not found');
        }
        return response.json();
      })
      .then(data => {
        setListing(data);
        setIsLoading(false);
      })
      .catch(err => {
        if (err.name === 'AbortError') return;
        setError(err.message);
        setIsLoading(false);
      });

    return () => controller.abort();
  }, [id]);

  if (isLoading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-center">
          <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-pink-600"></div>
          <p className="mt-4 text-gray-600">Loading listing...</p>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-center">
          <div className="text-6xl mb-4">😞</div>
          <h1 className="text-3xl font-bold text-gray-900 mb-2">
            Listing Not Found
          </h1>
          <p className="text-gray-600 mb-6">
            Listing #{id} doesn't exist or has been removed.
          </p>
          <a
            href="/"
            className="inline-block bg-pink-600 hover:bg-pink-700 text-white font-semibold py-2 px-6 rounded-lg transition-colors"
          >
            Back to Home
          </a>
        </div>
      </div>
    );
  }

  // Just pass data to card component!
  return <ListingDetailsCard listing={listing} />; 
}

What changed:

  • ✅ Imported ListingDetailsCard
  • ✅ Removed all display JSX
  • ✅ Now just: return <ListingDetailsCard listing={listing} />
  • ✅ Page handles data, Card handles display

Much cleaner! The page is now ~70 lines instead of 200+

Component Separation Benefits

One large component:

function ListingDetailsPage() {
  // State (20 lines)
  const [listing, setListing] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  // Data fetching (30 lines)
  useEffect(() => {
    fetch(`/api/listings/${id}`)
      .then(/* ... */);
  }, [id]);

  // Loading UI (10 lines)
  if (isLoading) return <Spinner />;

  // Error UI (15 lines)
  if (error) return <ErrorMessage />;

  // Display UI (150+ lines)
  return (
    <div>
      {/* Massive JSX */}
    </div>
  );
}

Total: ~225 lines, does everything

Two focused components:

// Page: Data fetching (70 lines)
function ListingDetailsPage() {
  const { id } = useParams();
  const [listing, setListing] = useState(null);
  
  useEffect(() => {
    fetch(`/api/listings/${id}`)
      .then(setListing);
  }, [id]);

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage />;

  return <ListingDetailsCard listing={listing} />;
}

// Card: Display only (80 lines)
function ListingDetailsCard({ listing }) {
  return (
    <div>
      {/* Clean display JSX */}
    </div>
  );
}

Total: ~150 lines split across 2 files

Before:

  • ❌ 225 lines in one file
  • ❌ Multiple responsibilities
  • ❌ Hard to test display logic
  • ❌ Can't reuse display elsewhere

After:

  • ✅ 70 lines (data) + 80 lines (display)
  • ✅ Single responsibility each
  • ✅ Easy to test separately
  • ✅ Card reusable anywhere

Wins:

  • Shorter files
  • Clearer purpose
  • Easier maintenance
  • Better testability

When to Extract Components

Props Best Practices

Component Extracted!

Your code is now cleaner and more maintainable! The page handles data fetching, and the card handles display - perfect separation of concerns.

Quick Recap

What we accomplished:

  • ✅ Created ListingDetailsCard component
  • ✅ Extracted display logic from page
  • ✅ Simplified ListingDetailsPage to ~70 lines
  • ✅ Made display logic reusable
  • ✅ Improved code organization

Key concepts:

  • Component extraction - Split large components
  • Separation of concerns - Data vs display
  • Props interface - Clear component API
  • Reusability - Use component anywhere
  • Maintainability - Smaller, focused files

What's Next?

In Lesson 9, we'll add Link components to navigate from the homepage listing cards to the details page. No more typing URLs manually - users can click cards to view details! 🔗