Code To Learn logo

Code To Learn

M6: State ManagementZustand Path

L7: Create Favorites Page

Build a dedicated page to display all favorited listings

Let's build a page to show all favorited listings! ⭐

What We're Building

A FavoritesPage that:

  • Shows all favorited listings
  • Displays empty state when no favorites
  • Reuses PropertyCard component
  • Updates in real-time when favorites change

Step 1: Create the Page Component

src/pages/FavoritesPage.jsx
import useListingsStore from '@/state/useListingsStore';
import PropertyCard from '@/components/PropertyCard';

function FavoritesPage() {
  // Select items and favorites
  const items = useListingsStore((state) => state.items);
  const favorites = useListingsStore((state) => state.favorites);
  
  // Get favorited listings
  const favoritedListings = items.filter((item) => 
    favorites.includes(item.id)
  );
  
  return (
    <div className="favorites-page">
      <header className="page-header">
        <h1>My Favorites</h1>
        <p>
          {favoritedListings.length === 0
            ? 'You haven\'t added any favorites yet'
            : `${favoritedListings.length} ${favoritedListings.length === 1 ? 'place' : 'places'} saved`
          }
        </p>
      </header>
      
      {favoritedListings.length === 0 ? (
        <div className="empty-state">
          <div className="empty-icon">❤️</div>
          <h2>No Favorites Yet</h2>
          <p>
            Start exploring and click the ❤️ button on listings you love!
          </p>
          <a href="/" className="button-primary">
            Explore Listings
          </a>
        </div>
      ) : (
        <div className="listings-grid">
          {favoritedListings.map((listing) => (
            <PropertyCard key={listing.id} listing={listing} />
          ))}
        </div>
      )}
    </div>
  );
}

export default FavoritesPage;

Understanding the Component

Better: Use Computed Selector

Instead of filtering in the component, let's use the store's getFavoritedItems:

src/pages/FavoritesPage.jsx (Improved)
import useListingsStore from '@/state/useListingsStore';
import PropertyCard from '@/components/PropertyCard';

function FavoritesPage() {
  // Use computed selector from store
  const getFavoritedItems = useListingsStore((state) => state.getFavoritedItems);
  const favoritedListings = getFavoritedItems();
  
  return (
    <div className="favorites-page">
      <header className="page-header">
        <h1>My Favorites</h1>
        <p>
          {favoritedListings.length === 0
            ? 'You haven\'t added any favorites yet'
            : `${favoritedListings.length} ${favoritedListings.length === 1 ? 'place' : 'places'} saved`
          }
        </p>
      </header>
      
      {favoritedListings.length === 0 ? (
        <div className="empty-state">
          <div className="empty-icon">❤️</div>
          <h2>No Favorites Yet</h2>
          <p>
            Start exploring and click the ❤️ button on listings you love!
          </p>
          <a href="/" className="button-primary">
            Explore Listings
          </a>
        </div>
      ) : (
        <div className="listings-grid">
          {favoritedListings.map((listing) => (
            <PropertyCard key={listing.id} listing={listing} />
          ))}
        </div>
      )}
    </div>
  );
}

export default FavoritesPage;

Comparison: Component Filter vs Store Selector

Filter in component:

function FavoritesPage() {
  const items = useListingsStore((state) => state.items);
  const favorites = useListingsStore((state) => state.favorites);
  
  // Filter here
  const favoritedListings = items.filter((item) =>
    favorites.includes(item.id)
  );
  
  return <div>{/* use favoritedListings */}</div>;
}

Pros:

  • Simple and direct
  • All logic visible in component

Cons:

  • Repeated if used in multiple components
  • Re-calculates on every render
  • Component has more responsibilities

Filter in store:

// In store
const useListingsStore = create((set, get) => ({
  items: [],
  favorites: [],
  
  getFavoritedItems: () => {
    const { items, favorites } = get();
    return items.filter((item) => favorites.includes(item.id));
  },
}));
// In component
function FavoritesPage() {
  const getFavoritedItems = useListingsStore((state) => state.getFavoritedItems);
  const favoritedListings = getFavoritedItems();
  
  return <div>{/* use favoritedListings */}</div>;
}

Pros:

  • Reusable across components
  • Logic centralized in store
  • Component stays simple

Cons:

  • Still re-calculates on every render
  • Need to remember to use the selector

Use component filtering when:

  • Only needed in one place
  • Simple, one-off logic
  • Component-specific requirements
// Example: Filter by search (component-specific)
const filtered = items.filter(item =>
  item.name.includes(searchQuery)
);

Use store selector when:

  • Needed in multiple components
  • Core business logic
  • Complex calculations
// Example: Get favorited items (used everywhere)
const getFavoritedItems = useListingsStore((state) => state.getFavoritedItems);

Best practice: Start with component filtering, move to store when you need it elsewhere!

Step 2: Add Styling

src/app/global.css
.favorites-page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.page-header {
  text-align: center;
  margin-bottom: 3rem;
}

.page-header h1 {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.page-header p {
  font-size: 1.2rem;
  color: #666;
}

/* Empty State */
.empty-state {
  text-align: center;
  padding: 4rem 2rem;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.empty-icon {
  font-size: 5rem;
  margin-bottom: 1rem;
  opacity: 0.3;
}

.empty-state h2 {
  font-size: 1.75rem;
  margin-bottom: 0.5rem;
  color: #333;
}

.empty-state p {
  font-size: 1.1rem;
  color: #666;
  margin-bottom: 2rem;
}

.button-primary {
  display: inline-block;
  padding: 0.75rem 2rem;
  background: #0369a1;
  color: white;
  text-decoration: none;
  border-radius: 4px;
  font-weight: 600;
  transition: background 0.2s;
}

.button-primary:hover {
  background: #075985;
}

Step 3: Add to Router

Update your router to include the FavoritesPage:

src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from '@/pages/HomePage';
import FavoritesPage from '@/pages/FavoritesPage';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/favorites" element={<FavoritesPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Test the Flow

Try this sequence:

  1. Go to HomePage (/)
  2. Click favorite button on 3 listings → ❤️ ❤️ ❤️
  3. Navigate to /favorites
  4. See your 3 favorited listings! ✅
  5. Click favorite button on one listing → 🤍
  6. It disappears from favorites page! ✅
  7. Go back to HomePage
  8. That listing shows 🤍 (not favorited)

Everything stays in sync! 🎉

Real-Time Updates

What's Next?

Perfect! FavoritesPage is complete. In the next lesson:

  1. Create Navbar - Navigation with favorites count
  2. Add logo - Branding
  3. Active link highlighting - Show current page
  4. Responsive design - Mobile-friendly

✅ Lesson Complete! FavoritesPage displays favorited listings with empty state handling!

Key Takeaways

  • Reused PropertyCard - Same component in HomePage and FavoritesPage
  • Empty state - Clear feedback when no favorites
  • Real-time updates - Removing favorite updates UI immediately
  • Shared data - No duplicate fetching or state
  • Computed selector - getFavoritedItems() centralizes logic
  • Loading states - Handles fetch lifecycle properly
  • Consistent UX - Same patterns as HomePage
  • Simpler than Redux - Less boilerplate, same functionality