Code To Learn logo

Code To Learn

M6: State ManagementZustand Path

L6: Favorites Feature Setup

Test and understand the favorites functionality in Zustand

Let's test and understand how favorites work! โค๏ธ

What We Already Have

Our store already has favorites functionality:

src/state/useListingsStore.js
const useListingsStore = create((set) => ({
  // State
  favorites: [],  // Array of listing IDs
  
  // Action
  toggleFavorite: (id) => set((state) => ({
    favorites: state.favorites.includes(id)
      ? state.favorites.filter(favId => favId !== id)  // Remove
      : [...state.favorites, id]                        // Add
  })),
}));

Let's test it and understand how it works! ๐Ÿงช

Step 1: Create Test Component

Let's create a simple test to verify favorites work:

src/components/FavoritesTest.jsx
import useListingsStore from '@/state/useListingsStore';

function FavoritesTest() {
  const favorites = useListingsStore((state) => state.favorites);
  const toggleFavorite = useListingsStore((state) => state.toggleFavorite);
  
  return (
    <div style={{ padding: '2rem', border: '2px solid #0369a1', margin: '1rem' }}>
      <h2>Favorites Test</h2>
      <p><strong>Current favorites:</strong> {JSON.stringify(favorites)}</p>
      
      <div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
        <button onClick={() => toggleFavorite('1')}>
          Toggle Listing 1
        </button>
        <button onClick={() => toggleFavorite('5')}>
          Toggle Listing 5
        </button>
        <button onClick={() => toggleFavorite('9')}>
          Toggle Listing 9
        </button>
      </div>
      
      <div style={{ marginTop: '1rem' }}>
        <p>Is Listing 1 favorited? {favorites.includes('1') ? 'โœ… Yes' : 'โŒ No'}</p>
        <p>Is Listing 5 favorited? {favorites.includes('5') ? 'โœ… Yes' : 'โŒ No'}</p>
        <p>Is Listing 9 favorited? {favorites.includes('9') ? 'โœ… Yes' : 'โŒ No'}</p>
      </div>
      
      <p style={{ marginTop: '1rem' }}>
        <strong>Total favorites:</strong> {favorites.length}
      </p>
    </div>
  );
}

export default FavoritesTest;

Step 2: Add to HomePage

Temporarily add this test component to HomePage:

src/pages/HomePage.jsx
import FavoritesTest from '@/components/FavoritesTest';

function HomePage() {
  // ... existing code ...
  
  return (
    <div className="home-page">
      {/* Add test component */}
      <FavoritesTest />
      
      {/* Rest of HomePage */}
      <header className="page-header">
        <h1>Discover Amazing Places</h1>
        {/* ... */}
      </header>
      {/* ... */}
    </div>
  );
}

Step 3: Test the Functionality

Try these actions:

  1. Click "Toggle Listing 1" โ†’ Array becomes ["1"]
  2. Click "Toggle Listing 5" โ†’ Array becomes ["1", "5"]
  3. Click "Toggle Listing 9" โ†’ Array becomes ["1", "5", "9"]
  4. Click "Toggle Listing 1" again โ†’ Array becomes ["5", "9"]

It works! โœ…

Understanding toggleFavorite

Let's break down how this action works:

How Components Re-Render

Only components that SELECT favorites re-render:

// Component A: Selects favorites
function FavoriteButton() {
  const favorites = useListingsStore((state) => state.favorites);
  // Re-renders when favorites change โœ…
  
  return <div>{favorites.length} favorites</div>;
}

// Component B: Selects items
function ListingsList() {
  const items = useListingsStore((state) => state.items);
  // Does NOT re-render when favorites change โŒ
  // Only re-renders when items change
  
  return <div>{items.length} items</div>;
}

// Component C: Selects nothing
function StaticComponent() {
  // Never re-renders from store changes โŒ
  
  return <div>Static content</div>;
}

When you toggle a favorite:

  • Component A: โœ… Re-renders (uses favorites)
  • Component B: โŒ Doesn't re-render (uses items)
  • Component C: โŒ Doesn't re-render (uses nothing)

Automatic optimization! ๐Ÿš€

All components share the same state:

// Component 1
function FavoriteButton({ listingId }) {
  const favorites = useListingsStore((state) => state.favorites);
  const toggleFavorite = useListingsStore((state) => state.toggleFavorite);
  
  return (
    <button onClick={() => toggleFavorite(listingId)}>
      {favorites.includes(listingId) ? 'โค๏ธ' : '๐Ÿค'}
    </button>
  );
}

// Component 2
function FavoriteCount() {
  const favorites = useListingsStore((state) => state.favorites);
  
  return <span>{favorites.length} favorites</span>;
}

// Component 3
function FavoritesList() {
  const favorites = useListingsStore((state) => state.favorites);
  const items = useListingsStore((state) => state.items);
  
  const favoritedItems = items.filter(item => 
    favorites.includes(item.id)
  );
  
  return <div>{favoritedItems.map(/* ... */)}</div>;
}

When you click the button in Component 1:

  1. toggleFavorite(5) is called
  2. Store updates: favorites = [1, 5, 9]
  3. ALL three components re-render (they all use favorites)
  4. Button shows โค๏ธ (favorited)
  5. Count shows "3 favorites"
  6. List shows all 3 favorited items

All components stay in sync automatically! โœจ

How efficient is this?

// โŒ Inefficient: Re-renders on any state change
const state = useListingsStore();

// โœ… Efficient: Only re-renders when favorites change
const favorites = useListingsStore((state) => state.favorites);

// โœ… Even more efficient: Only re-renders when count changes
const count = useListingsStore((state) => state.favorites.length);

Comparison:

// Scenario: 100 components using the store

// Bad: All select entire state
// toggleFavorite() โ†’ 100 re-renders โŒ

// Good: All select favorites
// toggleFavorite() โ†’ 100 re-renders (but necessary!)

// Best: Mix of specific selectors
// - 10 components select favorites โ†’ 10 re-renders
// - 90 components select items โ†’ 0 re-renders
// toggleFavorite() โ†’ only 10 re-renders โœ…

Rule: Select only what you need!

Getting Favorited Listings

Now let's add a selector to get the actual favorited listings:

src/state/useListingsStore.js
const useListingsStore = create((set, get) => ({
  // ... existing state and actions ...
  
  // Computed selector for favorited listings
  getFavoritedItems: () => {
    const { items, favorites } = get();
    return items.filter((item) => favorites.includes(item.id));
  },
}));

Usage:

// In any component
const getFavoritedItems = useListingsStore((state) => state.getFavoritedItems);
const favoritedItems = getFavoritedItems();

return (
  <div>
    <h2>Your Favorites ({favoritedItems.length})</h2>
    {favoritedItems.map((item) => (
      <PropertyCard key={item.id} listing={item} />
    ))}
  </div>
);

Add to PropertyCard

Let's add a favorite button to PropertyCard:

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

function PropertyCard({ listing }) {
  // Select favorites and action
  const favorites = useListingsStore((state) => state.favorites);
  const toggleFavorite = useListingsStore((state) => state.toggleFavorite);
  
  // Check if this listing is favorited
  const isFavorited = favorites.includes(listing.id);
  
  return (
    <div className="listing-card">
      <div className="listing-image">
        <img 
          src={listing.media?.[0]?.url || '/placeholder.jpg'} 
          alt={listing.media?.[0]?.alt || listing.name}
        />
        
        {/* Favorite Button */}
        <button
          className={`favorite-button ${isFavorited ? 'favorited' : ''}`}
          onClick={() => toggleFavorite(listing.id)}
          aria-label={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
        >
          {isFavorited ? 'โค๏ธ' : '๐Ÿค'}
        </button>
      </div>
      
      <div className="listing-content">
        <h3>{listing.name}</h3>
        <p className="listing-description">{listing.description}</p>
        
        <div className="listing-details">
          <span className="price">${listing.price}/night</span>
          <span className="guests">๐Ÿ‘ฅ {listing.maxGuests} guests</span>
        </div>
      </div>
    </div>
  );
}

export default PropertyCard;

Styling the Favorite Button

Add CSS for the favorite button:

src/app/global.css
.listing-card {
  position: relative;
  background: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s;
}

.listing-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}

.listing-image {
  position: relative;
  height: 200px;
  overflow: hidden;
}

.listing-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Favorite Button */
.favorite-button {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  background: white;
  border: none;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  transition: all 0.2s;
  z-index: 10;
}

.favorite-button:hover {
  transform: scale(1.1);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.favorite-button.favorited {
  background: #fee;
  animation: heartbeat 0.3s ease;
}

@keyframes heartbeat {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.2); }
}

.listing-content {
  padding: 1rem;
}

.listing-content h3 {
  margin: 0 0 0.5rem 0;
  font-size: 1.25rem;
}

.listing-description {
  color: #666;
  font-size: 0.9rem;
  margin-bottom: 1rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.listing-details {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 1rem;
  border-top: 1px solid #eee;
}

.price {
  font-weight: 600;
  font-size: 1.1rem;
  color: #0369a1;
}

.guests {
  color: #666;
  font-size: 0.9rem;
}

Test It Out!

Now when you view your listings:

  1. Each card has a favorite button (๐Ÿค)
  2. Click the button โ†’ it turns red (โค๏ธ)
  3. Favorites array updates: [1, 5, 9]
  4. Click again โ†’ it turns white (๐Ÿค)
  5. Favorites array updates: [5, 9]
  6. Button animates with heartbeat effect! ๐Ÿ’“

All cards share the same favorites state! โœจ

Understanding the Flow

What's Next?

Perfect! Favorites are working. In the next lesson:

  1. Create FavoritesPage - Display favorited listings
  2. Handle empty state - When no favorites yet
  3. Add remove functionality - Unfavorite from list
  4. Reuse PropertyCard - Same component, different context

โœ… Lesson Complete! Favorites functionality is working!

Key Takeaways

  • โœ… toggleFavorite works - Add/remove listings from favorites
  • โœ… ID-based storage - Efficient and simple
  • โœ… Immutable updates - Creates new arrays for re-renders
  • โœ… Selective re-renders - Only components using favorites re-render
  • โœ… Shared state - All components see same favorites
  • โœ… PropertyCard enhanced - Favorite button with animation
  • โœ… Better than local state - Global, persistent, shareable
  • โœ… Simpler than Redux - No dispatch, no Provider, just hooks