L15: Module Review
Complete review of Redux Toolkit patterns and the favorites feature
Congratulations! You've completed Module 6 and built a complete Redux-powered favorites system! 🎉
Let's review everything you learned and see the complete picture.
What We Built
A full-featured favorites system with Redux Toolkit:
✅ Global state management with Redux store ✅ Listings slice with actions and reducers ✅ Async data fetching with thunks ✅ Favorites toggle functionality ✅ Dedicated favorites page ✅ Navigation with counter badge ✅ Favorite button component ✅ Complete UI integration
Redux Toolkit Architecture
The Complete Flow
┌─────────────────────────────────────────────┐
│ Redux Store │
│ │
│ state: { │
│ listings: { │
│ items: [...], │
│ favorites: [1, 5, 9], │
│ status: 'succeeded', │
│ error: null │
│ } │
│ } │
└─────────────────────────────────────────────┘
↑ ↓
dispatch() useSelector()
↑ ↓
┌──────────┴────────────────────┴─────────────┐
│ React Components │
│ │
│ HomePage → reads items, status │
│ FavoritesPage → reads favorites │
│ Navbar → reads favorites.length │
│ FavoriteButton → reads & dispatches │
└─────────────────────────────────────────────┘Core Concepts Review
The Single Source of Truth:
import { configureStore } from '@reduxjs/toolkit';
import listingsReducer from './slices/listingsSlice';
export const store = configureStore({
reducer: {
listings: listingsReducer
}
});What we learned:
configureStore()- Easy store setup- Automatic Redux DevTools integration
- Automatic thunk middleware
- Single global state object
Key insight: 70% less boilerplate than traditional Redux!
Feature-Based State Organization:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for API calls
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters) => {
const data = await api.listings.getAll(filters);
return data;
}
);
// Slice with state, reducers, and extraReducers
const listingsSlice = createSlice({
name: 'listings',
initialState: {
items: [],
favorites: [],
status: 'idle',
error: null
},
reducers: {
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);
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchListings.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchListings.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchListings.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export const { toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;What we learned:
createSlice()- Combines actions and reducers- Immer for "mutable" updates (actually immutable)
reducers- Synchronous actionsextraReducers- Handle external actions (thunks)- Automatic action creators
Async Operations Made Easy:
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters) => {
const data = await api.listings.getAll(filters);
return data;
}
);What we learned:
createAsyncThunk- Handle async operations- Automatically dispatches 3 actions:
pending- When startingfulfilled- When successfulrejected- When error occurs
- Return value becomes
fulfilledpayload - Errors automatically handled
Pattern:
User action → dispatch thunk → API call
→ pending → loading state
→ fulfilled → success state + data
OR
→ rejected → error state + messageReading and Writing State:
useSelector - Read State:
import { useSelector } from 'react-redux';
// Read entire slice
const listings = useSelector((state) => state.listings);
// Read specific fields
const items = useSelector((state) => state.listings.items);
const favorites = useSelector((state) => state.listings.favorites);
const status = useSelector((state) => state.listings.status);
// Computed values
const favoriteCount = useSelector((state) =>
state.listings.favorites.length
);useDispatch - Write State:
import { useDispatch } from 'react-redux';
import { toggleFavorite, fetchListings } from '@/state/slices/listingsSlice';
function Component() {
const dispatch = useDispatch();
// Dispatch sync action
const handleFavorite = () => {
dispatch(toggleFavorite(id));
};
// Dispatch async action
useEffect(() => {
dispatch(fetchListings());
}, []);
}What we learned:
useSelector- Subscribe to stateuseDispatch- Send actions- Components re-render when selected state changes
- Hooks make Redux simple!
Complete Data Flow:
1. User clicks favorite button:
<button onClick={() => dispatch(toggleFavorite(listingId))}>
❤️
</button>2. Action dispatched:
{
type: 'listings/toggleFavorite',
payload: 1
}3. Reducer updates state:
// Before: favorites = []
// After: favorites = [1]4. Components re-render:
// All these update automatically!
<FavoriteButton /> // Heart fills
<Navbar /> // Counter increases
<FavoritesPage /> // Shows new favoriteRedux DevTools shows everything!
Complete Code Review
Let's see how everything connects:
Store Setup
import { configureStore } from '@reduxjs/toolkit';
import listingsReducer from './slices/listingsSlice';
export const store = configureStore({
reducer: {
listings: listingsReducer
}
});Provider Integration
import { Provider } from 'react-redux';
import { store } from './state/store';
import Navbar from './components/Navbar';
import { Router } from './components/Router';
export function App() {
return (
<Provider store={store}>
<div className="min-h-screen flex flex-col">
<Navbar />
<main className="flex-1">
<Router />
</main>
</div>
</Provider>
);
}Slice Implementation
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { api } from '@/api';
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters) => {
const data = await api.listings.getAll(filters);
return data;
}
);
const listingsSlice = createSlice({
name: 'listings',
initialState: {
items: [],
favorites: [],
status: 'idle',
error: null
},
reducers: {
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);
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchListings.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchListings.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchListings.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export const { toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;Component Integration Examples
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import SearchBar from '@/components/SearchBar';
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
// Local state for filters (UI-specific)
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ checkIn: '', checkOut: '' });
const [guests, setGuests] = useState(1);
// Fetch on mount
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);
// Filter listings locally
const filteredListings = items.filter((listing) => {
const matchesSearch = listing.title
.toLowerCase()
.includes(search.toLowerCase());
const matchesGuests = listing.maxGuests >= guests;
return matchesSearch && matchesGuests;
});
if (status === 'loading') {
return <div>Loading...</div>;
}
if (status === 'failed') {
return <div>Error: {error}</div>;
}
return (
<div className="container mx-auto px-4 py-8">
<SearchBar
search={search}
setSearch={setSearch}
dates={dates}
setDates={setDates}
guests={guests}
setGuests={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}
export default HomePage;Key points:
- ✅ Redux for listings data (global)
- ✅ Local state for filters (UI-specific)
- ✅ Fetch once on mount
- ✅ Filter locally for performance
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { Heart } from 'lucide-react';
import ListingList from '@/components/ListingList';
function FavoritesPage() {
const items = useSelector((state) => state.listings.items);
const favoriteIds = useSelector((state) => state.listings.favorites);
const favoriteListings = items.filter((listing) =>
favoriteIds.includes(listing.id)
);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">
Your Favorites ({favoriteListings.length})
</h1>
{favoriteListings.length === 0 ? (
<div className="text-center py-12">
<Heart size={64} className="mx-auto mb-4 text-gray-300" />
<h2 className="text-xl font-semibold mb-2 text-gray-700">
No favorites yet
</h2>
<p className="text-gray-500 mb-6">
Start exploring and save your favorite listings!
</p>
<Link
to="/"
className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Browse Listings
</Link>
</div>
) : (
<ListingList listings={favoriteListings} />
)}
</div>
);
}
export default FavoritesPage;Key points:
- ✅ Filter items by favorite IDs
- ✅ Reuse ListingList component
- ✅ Handle empty state
- ✅ Show count in header
import { Link } from 'react-router-dom';
import ListingFavoriteButton from './ListingFavoriteButton';
function PropertyCard({ listing }) {
return (
<Link
to={`/listings/${listing.id}`}
className="block border rounded-lg overflow-hidden hover:shadow-lg transition-shadow"
>
<div className="relative">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<div className="absolute top-2 right-2 bg-white/90 backdrop-blur-sm rounded-full shadow-md">
<ListingFavoriteButton listingId={listing.id} />
</div>
</div>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 text-sm mb-2 line-clamp-2">
{listing.description}
</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</Link>
);
}
export default PropertyCard;Key points:
- ✅ Relative + absolute positioning
- ✅ Button in top-right corner
- ✅ Semi-transparent background for visibility
- ✅ Works inside Link component
Redux vs Local State Decision Tree
When to use Redux vs useState:
Need to share data across multiple components?
├─ YES → Use Redux
│ Examples:
│ - User authentication
│ - Shopping cart
│ - Favorites
│ - Fetched data (listings, products)
│ - Theme settings
│
└─ NO → Use Local State
Examples:
- Form inputs
- Toggle states (modal open/closed)
- Temporary UI state
- Search/filter values (unless shared)
- Hover statesPerformance Considerations
Selector Optimization
Create reusable selectors:
// Export selectors for reuse
export const selectAllListings = (state) => state.listings.items;
export const selectFavoriteIds = (state) => state.listings.favorites;
export const selectListingsStatus = (state) => state.listings.status;
// Computed selector
export const selectFavoriteListings = (state) => {
const items = state.listings.items;
const favoriteIds = state.listings.favorites;
return items.filter(listing => favoriteIds.includes(listing.id));
};
export const selectFavoriteCount = (state) =>
state.listings.favorites.length;Use in components:
import { selectFavoriteCount, selectFavoriteListings } from '@/state/slices/listingsSlice';
function Component() {
const count = useSelector(selectFavoriteCount);
const favorites = useSelector(selectFavoriteListings);
}Benefits:
- ✅ Reusable across components
- ✅ Easy to test
- ✅ Single source of truth
- ✅ Can be memoized with Reselect
Advanced: Memoized Selectors
For expensive computations, use createSelector from Reselect:
import { createSelector } from '@reduxjs/toolkit';
const selectListingsItems = (state) => state.listings.items;
const selectFavoriteIds = (state) => state.listings.favorites;
// Memoized - only recomputes when inputs change
export const selectFavoriteListings = createSelector(
[selectListingsItems, selectFavoriteIds],
(items, favoriteIds) => {
console.log('Computing favorite listings...');
return items.filter(listing => favoriteIds.includes(listing.id));
}
);When to use:
- Complex filtering/sorting
- Expensive calculations
- Computed values used in multiple components
Testing Redux
Testing Reducers
import listingsReducer, { toggleFavorite } from './listingsSlice';
test('toggleFavorite adds ID to favorites', () => {
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null
};
const action = toggleFavorite(1);
const newState = listingsReducer(initialState, action);
expect(newState.favorites).toEqual([1]);
});
test('toggleFavorite removes ID from favorites', () => {
const initialState = {
items: [],
favorites: [1, 2, 3],
status: 'idle',
error: null
};
const action = toggleFavorite(2);
const newState = listingsReducer(initialState, action);
expect(newState.favorites).toEqual([1, 3]);
});Testing Components with Redux
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import listingsReducer from './state/slices/listingsSlice';
import Navbar from './components/Navbar';
test('Navbar shows favorite count', () => {
const store = configureStore({
reducer: {
listings: listingsReducer
},
preloadedState: {
listings: {
items: [],
favorites: [1, 2, 3],
status: 'idle',
error: null
}
}
});
render(
<Provider store={store}>
<Navbar />
</Provider>
);
expect(screen.getByText('3')).toBeInTheDocument();
});Best Practices Summary
Structure Your Redux
src/
state/
store.js # Configure store
slices/
listingsSlice.js # Listings feature
userSlice.js # User feature
cartSlice.js # Cart featureOne slice per feature!
Use Redux Toolkit
Always prefer Redux Toolkit over vanilla Redux:
- ✅
configureStore()instead ofcreateStore() - ✅
createSlice()instead of action creators + reducers - ✅
createAsyncThunk()for async operations - ✅ Immer for immutable updates
70% less code!
Create Selector Functions
// ✅ Do this
export const selectFavorites = (state) => state.listings.favorites;
// Then use
const favorites = useSelector(selectFavorites);
// ❌ Not this
const favorites = useSelector((state) => state.listings.favorites);Reusable, testable, maintainable!
Keep Local State Local
Don't put everything in Redux!
// ✅ Good
const [isOpen, setIsOpen] = useState(false); // Local
// ❌ Bad
const isOpen = useSelector((state) => state.ui.modalOpen); // Overkill!Redux for shared data, useState for local UI.
Use Redux DevTools
Essential for debugging:
- See all state changes
- Time-travel debugging
- Action history
- State snapshots
Install Redux DevTools browser extension!
Common Patterns
Loading Pattern
const { data, status, error } = useSelector((state) => state.feature);
if (status === 'loading') return <Spinner />;
if (status === 'failed') return <Error message={error} />;
if (status === 'succeeded') return <Data items={data} />;Optimistic Updates
const handleToggleFavorite = (id) => {
// Update UI immediately
dispatch(toggleFavorite(id));
// Sync with backend in background
api.favorites.toggle(id).catch((error) => {
// Revert if fails
dispatch(toggleFavorite(id));
showError('Failed to update favorite');
});
};Pagination
const listingsSlice = createSlice({
name: 'listings',
initialState: {
items: [],
page: 1,
hasMore: true
},
reducers: {
appendListings: (state, action) => {
state.items.push(...action.payload);
state.page += 1;
},
setHasMore: (state, action) => {
state.hasMore = action.payload;
}
}
});What's Next?
You've mastered Redux Toolkit! Here's what you can explore:
Immediate Next Steps
- Module 7: Forms & Authentication - User login with Redux
- Module 8: Deployment - Deploy your app
- Module X: Advanced Topics - Hooks, testing, optimization
Advanced Redux Topics
- RTK Query - Auto-generated API hooks
- Redux Persist - Save state to localStorage
- Reselect - Memoized selectors for performance
- Redux Saga - Complex async workflows
- Normalization - Flat state shape for performance
Practice Projects
Build more features with Redux:
- Shopping Cart - Add items, update quantities, checkout
- Authentication - Login, logout, protected routes
- Notifications - Global toast messages
- Dark Mode - Theme toggle across app
- Multi-step Form - Complex form with state
Key Takeaways
🎯 You've learned:
✅ Redux Store - Single source of truth for global state ✅ Slices - Feature-based state organization with createSlice() ✅ Thunks - Async operations with createAsyncThunk() ✅ Reducers - Pure functions that update state (with Immer!) ✅ Actions - Auto-generated action creators ✅ Hooks - useSelector and useDispatch for components ✅ Integration - Connect Redux to React with Provider ✅ DevTools - Debug state changes with time travel ✅ Patterns - Loading states, computed values, toggles ✅ Best Practices - When to use Redux vs local state
Complete Favorites Feature
Here's what you built - a production-ready favorites system:
User Flow:
1. Browse listings on HomePage
2. Click heart icon on PropertyCard
3. Redux updates favorites array
4. All components re-render:
- Heart fills on clicked card ✅
- Navbar counter increases ✅
- Favorites page updates ✅
5. Navigate to Favorites page
6. See all favorited listings
7. Unfavorite from anywhere
8. State stays synchronized everywhere
Technical Implementation:
- Store with listings slice ✅
- fetchListings async thunk ✅
- toggleFavorite reducer ✅
- Selectors for reusability ✅
- HomePage with Redux ✅
- FavoritesPage component ✅
- Navbar with counter ✅
- FavoriteButton component ✅
- Integrated in PropertyCard ✅
- Complete routing ✅Congratulations! 🎉
You've completed Module 6: State Management!
You now have a solid foundation in Redux Toolkit and can:
- Build scalable state management systems
- Handle async operations professionally
- Integrate Redux with React components
- Create reusable, testable code
- Debug state issues effectively
Ready for the next challenge? Let's continue building! 🚀
Need Help?
- Review Redux DevTools for state issues
- Check lesson 8-14 for implementation details
- Practice by adding more Redux features
- Reference this review anytime!