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:
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:
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:
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:
- Click "Toggle Listing 1" โ Array becomes
["1"] - Click "Toggle Listing 5" โ Array becomes
["1", "5"] - Click "Toggle Listing 9" โ Array becomes
["1", "5", "9"] - 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:
toggleFavorite(5)is called- Store updates:
favorites = [1, 5, 9] - ALL three components re-render (they all use favorites)
- Button shows โค๏ธ (favorited)
- Count shows "3 favorites"
- 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:
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:
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:
.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:
- Each card has a favorite button (๐ค)
- Click the button โ it turns red (โค๏ธ)
- Favorites array updates:
[1, 5, 9] - Click again โ it turns white (๐ค)
- Favorites array updates:
[5, 9] - 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:
- Create FavoritesPage - Display favorited listings
- Handle empty state - When no favorites yet
- Add remove functionality - Unfavorite from list
- 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