Code To Learn logo

Code To Learn

M6: State ManagementRedux Toolkit Path

L8: Favorites Setup

Test the toggleFavorite reducer and prepare for favorites UI

We already have a toggleFavorite reducer in our slice! Let's test it and prepare to use it in our UI.

Review: toggleFavorite Reducer

Back in Lesson 3, we created this reducer:

src/state/slices/listingsSlice.js
reducers: {
  toggleFavorite: (state, action) => {
    const id = action.payload;
    
    if (state.favorites.includes(id)) {
      // Remove from favorites
      state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
    } else {
      // Add to favorites
      state.favorites.push(id);
    }
  },
}

What it does:

  • Takes a listing ID as payload
  • If ID is in favorites → remove it
  • If ID not in favorites → add it
  • Perfect for toggle buttons!

Understanding the Favorites State

The favorites array stores listing IDs:

// Initial state
{
  favorites: []
}

// After favoriting listings 1, 5, and 9
{
  favorites: [1, 5, 9]
}

// After unfavoriting listing 5
{
  favorites: [1, 9]
}

Why store IDs instead of full listing objects?

How to Get Favorited Listings

Even though we store IDs, we can easily get the full listing data:

function FavoritesPage() {
  const items = useSelector((state) => state.listings.items);
  const favoriteIds = useSelector((state) => state.listings.favorites);
  
  // Get full listing objects for favorited IDs
  const favoriteListings = items.filter(listing => 
    favoriteIds.includes(listing.id)
  );
  
  return (
    <div>
      {favoriteListings.map(listing => (
        <PropertyCard key={listing.id} listing={listing} />
      ))}
    </div>
  );
}

Pattern:

  1. Get all listings from state.listings.items
  2. Get favorite IDs from state.listings.favorites
  3. Filter listings to only those with IDs in favorites array

Simple and efficient! ✨

Testing toggleFavorite

Let's test the reducer works correctly. Open Redux DevTools:

Open Redux DevTools

  1. Open your app in browser
  2. Open DevTools (F12)
  3. Click Redux tab

Check Initial State

In the State tab, you should see:

{
  "listings": {
    "items": [/* listings */],
    "favorites": [],
    "status": "succeeded",
    "error": null
  }
}

Favorites is empty initially.

Manually Dispatch Toggle

In the Dispatch section, manually dispatch:

{
  "type": "listings/toggleFavorite",
  "payload": 1
}

Click Dispatch.

Verify State Updated

Check the State tab again:

{
  "listings": {
    "items": [/* listings */],
    "favorites": [1],
    "status": "succeeded",
    "error": null
  }
}

ID 1 was added! 🎉

Toggle Again (Remove)

Dispatch the same action again:

{
  "type": "listings/toggleFavorite",
  "payload": 1
}

Check state - 1 should be removed:

{
  "listings": {
    "favorites": []
  }
}

Perfect! The toggle works both ways! ✨

Add Multiple Favorites

Try adding multiple:

{ "type": "listings/toggleFavorite", "payload": 1 }
{ "type": "listings/toggleFavorite", "payload": 5 }
{ "type": "listings/toggleFavorite", "payload": 9 }

State should show:

{
  "listings": {
    "favorites": [1, 5, 9]
  }
}

Excellent! 🎉

Selector Patterns for Favorites

Here are useful selectors you'll use:

Get the array of favorite IDs:

import { useSelector } from 'react-redux';

function Component() {
  const favoriteIds = useSelector((state) => state.listings.favorites);
  
  console.log(favoriteIds);  // [1, 5, 9]
}

Use when: You just need the IDs (for checking, counting, etc.)

Get the full listing objects:

import { useSelector } from 'react-redux';

function Component() {
  const items = useSelector((state) => state.listings.items);
  const favoriteIds = useSelector((state) => state.listings.favorites);
  
  const favoriteListings = items.filter(listing => 
    favoriteIds.includes(listing.id)
  );
  
  console.log(favoriteListings);
  // [
  //   { id: 1, title: 'Beach House', ... },
  //   { id: 5, title: 'Mountain Cabin', ... },
  //   { id: 9, title: 'City Apartment', ... }
  // ]
}

Use when: You need to display favorited listings

Check if a specific listing is favorited:

import { useSelector } from 'react-redux';

function PropertyCard({ listing }) {
  const favoriteIds = useSelector((state) => state.listings.favorites);
  
  const isFavorited = favoriteIds.includes(listing.id);
  
  return (
    <div>
      <h3>{listing.title}</h3>
      <button>
        {isFavorited ? '❤️ Favorited' : '🤍 Favorite'}
      </button>
    </div>
  );
}

Use when: You need to show if a listing is favorited (for button state)

Get the number of favorites:

import { useSelector } from 'react-redux';

function Navbar() {
  const favoriteCount = useSelector((state) => state.listings.favorites.length);
  
  return (
    <nav>
      <a href="/favorites">
        Favorites ({favoriteCount})
      </a>
    </nav>
  );
}

Use when: You need to display a count (badge, counter, etc.)

Creating Selector Functions (Best Practice)

Instead of writing selectors inline, create reusable selector functions:

src/state/slices/listingsSlice.js
// At the bottom of the file, after the slice

// Selectors
export const selectAllListings = (state) => state.listings.items;
export const selectFavoriteIds = (state) => state.listings.favorites;
export const selectListingsStatus = (state) => state.listings.status;
export const selectListingsError = (state) => state.listings.error;

// Computed selector - favorited listings
export const selectFavoriteListings = (state) => {
  const items = state.listings.items;
  const favoriteIds = state.listings.favorites;
  return items.filter(listing => favoriteIds.includes(listing.id));
};

// Computed selector - is listing favorited?
export const selectIsListingFavorited = (state, listingId) => {
  return state.listings.favorites.includes(listingId);
};

// Computed selector - favorite count
export const selectFavoriteCount = (state) => state.listings.favorites.length;

Usage in components:

import { useSelector } from 'react-redux';
import { 
  selectFavoriteListings, 
  selectFavoriteCount 
} from '@/state/slices/listingsSlice';

function FavoritesPage() {
  const favoriteListings = useSelector(selectFavoriteListings);
  const count = useSelector(selectFavoriteCount);
  
  return (
    <div>
      <h1>Your Favorites ({count})</h1>
      {favoriteListings.map(listing => (
        <PropertyCard key={listing.id} listing={listing} />
      ))}
    </div>
  );
}

Benefits:

  • ✅ Reusable across components
  • ✅ Easy to test
  • ✅ Single source of truth
  • ✅ Can be memoized for performance
  • ✅ Self-documenting

Dispatching toggleFavorite

To use toggleFavorite in components:

import { useDispatch } from 'react-redux';
import { toggleFavorite } from '@/state/slices/listingsSlice';

function PropertyCard({ listing }) {
  const dispatch = useDispatch();
  
  const handleToggleFavorite = () => {
    dispatch(toggleFavorite(listing.id));
  };
  
  return (
    <div>
      <h3>{listing.title}</h3>
      <button onClick={handleToggleFavorite}>
        Toggle Favorite
      </button>
    </div>
  );
}

Flow:

  1. User clicks button
  2. handleToggleFavorite runs
  3. Dispatches toggleFavorite(listing.id)
  4. Reducer adds/removes ID from favorites
  5. Components re-render with new state

Understanding the Toggle Pattern

The toggle pattern is common in Redux:

Our current pattern - add or remove:

toggleFavorite: (state, action) => {
  const id = action.payload;
  
  if (state.favorites.includes(id)) {
    state.favorites = state.favorites.filter(fav => fav !== id);
  } else {
    state.favorites.push(id);
  }
}

Use when: Managing arrays of IDs (favorites, selected items, tags)

Toggle boolean values:

toggleSidebar: (state) => {
  state.sidebarOpen = !state.sidebarOpen;
}

toggleDarkMode: (state) => {
  state.darkMode = !state.darkMode;
}

Use when: Simple on/off states (modals, sidebars, themes)

Cycle through multiple states:

cycleView: (state) => {
  const views = ['grid', 'list', 'map'];
  const currentIndex = views.indexOf(state.view);
  const nextIndex = (currentIndex + 1) % views.length;
  state.view = views[nextIndex];
}

Use when: Cycling through options (views, themes, sort orders)

Preparing for UI

In the next lessons, we'll build:

Favorites Page (Lesson 9)

Display all favorited listings:

function FavoritesPage() {
  const favoriteListings = useSelector(selectFavoriteListings);
  
  return (
    <div>
      <h1>Your Favorites</h1>
      {favoriteListings.map(listing => (
        <PropertyCard key={listing.id} listing={listing} />
      ))}
    </div>
  );
}

Navigation with favorites count:

function Navbar() {
  const count = useSelector(selectFavoriteCount);
  
  return (
    <nav>
      <Link to="/favorites">
        ❤️ Favorites ({count})
      </Link>
    </nav>
  );
}

Favorite Button (Lesson 13-14)

Toggle button on listing cards:

function ListingFavoriteButton({ listingId }) {
  const dispatch = useDispatch();
  const isFavorited = useSelector((state) => 
    selectIsListingFavorited(state, listingId)
  );
  
  return (
    <button onClick={() => dispatch(toggleFavorite(listingId))}>
      {isFavorited ? '❤️' : '🤍'}
    </button>
  );
}

What's Next?

Perfect! The favorites system is ready to use. In the next lesson, we'll:

  1. Create FavoritesPage - New page to display favorites
  2. Use selectors - Get favorited listings from Redux
  3. Handle empty state - Show message when no favorites

✅ Lesson Complete! You've tested the toggleFavorite reducer and learned how to work with favorites in Redux!

Key Takeaways

  • Store IDs, not objects - More efficient and easier to manage
  • Filter items to get full listing objects from IDs
  • Toggle pattern - Add if not present, remove if present
  • Create selectors for reusable state access
  • Computed selectors derive data from existing state
  • ✅ Test reducers with Redux DevTools manual dispatch
  • .includes() checks if ID is in favorites array