L7: Refactor HomePage
Replace local state with Redux in HomePage component
Time to put Redux to work! Let's refactor HomePage to use Redux instead of local state.
Current HomePage State
Currently, HomePage manages its own state with useState and useEffect:
import { useState, useEffect } from 'react';
import { useFetch } from '@/hooks/useFetch';
function HomePage() {
const { data: listings, isLoading, error } = useFetch('/listings');
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// Filter listings locally
const filteredListings = useMemo(() => {
if (!listings) return [];
return listings.filter(/* filtering logic */);
}, [listings, search, dates, guests]);
// Render...
}Problems:
- Listings data lives only in this component
- Other pages can't access listings
- Duplicate fetch logic if other pages need listings
- Can't share favorites across pages
Solution: Move listings to Redux!
What We're Building
We'll refactor HomePage to:
- Remove useFetch - Replace with Redux
- Dispatch fetchListings - Load data from Redux
- Select from Redux - Read listings/status/error from store
- Keep filters local - Search/dates/guests stay in component
Why keep filters local? They're UI-specific state that only HomePage needs!
Redux vs Local State Decision
Put in Redux when:
- ✅ Multiple components need the data
- ✅ Data persists across page changes
- ✅ Complex state updates
- ✅ Needs to be cached
- ✅ Fetched from API
Examples:
- User profile
- Listings data
- Favorites
- Shopping cart
- Notifications
Keep in local state when:
- ✅ Only one component uses it
- ✅ Temporary/transient data
- ✅ UI-specific (modals, toggles, form input)
- ✅ Resets on unmount
Examples:
- Form input values
- Modal open/closed
- Selected tab
- Search filters
- Accordion expanded state
Does multiple components need this data?
├─ Yes → Redux
└─ No
└─ Does it persist across pages?
├─ Yes → Redux
└─ No
└─ Is it fetched from API?
├─ Yes → Redux (usually)
└─ No → Local StateFor HomePage:
- Listings → Redux (shared, cached, API)
- Favorites → Redux (shared across pages)
- Search/dates/guests → Local (UI-specific, temporary)
Step 1: Remove useFetch
Open src/pages/HomePage.jsx and remove the useFetch import and call:
import { useState, useMemo } from 'react';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
function HomePage() {
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// Removed: const { data: listings, isLoading, error } = useFetch('/listings');
// ... rest of component
}Step 2: Import Redux Hooks
Import useSelector, useDispatch, and fetchListings:
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
function HomePage() {
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// Redux next...
}What are we importing?
Step 3: Get dispatch and Select State
Add useDispatch and useSelector:
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
// useEffect next...
}What's happening?
Select from Redux
const { items, status, error } = useSelector((state) => state.listings);This reads three fields from state.listings:
items- Array of listingsstatus- 'idle' | 'loading' | 'succeeded' | 'failed'error- Error message (if any)
Why destructure?
More readable than:
const listings = useSelector((state) => state.listings);
const items = listings.items;
const status = listings.status;Step 4: Dispatch fetchListings on Mount
Add a useEffect to fetch listings when component mounts:
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);
// Filtering logic next...
}Why check status === 'idle'?
Step 5: Update Filtering Logic
Update the useMemo to use items instead of listings:
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);
const filteredListings = useMemo(() => {
return items.filter((listing) => {
const matchesSearch = listing.title
.toLowerCase()
.includes(search.toLowerCase());
const matchesGuests = listing.maxGuests >= guests;
// Simplified date checking for now
const matchesDates = true;
return matchesSearch && matchesGuests && matchesDates;
});
}, [items, search, guests]);
// Render next...
}Changed: listings.filter → items.filter
Step 6: Update Render Logic
Update the rendering to use status and error:
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
import Spinner from '@/components/Spinner';
import ErrorMessage from '@/components/ErrorMessage';
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);
const filteredListings = useMemo(() => {
return items.filter((listing) => {
const matchesSearch = listing.title
.toLowerCase()
.includes(search.toLowerCase());
const matchesGuests = listing.maxGuests >= guests;
const matchesDates = true;
return matchesSearch && matchesGuests && matchesDates;
});
}, [items, search, guests]);
if (status === 'loading') {
return (
<div className="flex justify-center items-center min-h-screen">
<Spinner />
</div>
);
}
if (status === 'failed') {
return (
<div className="container mx-auto px-4 py-8">
<ErrorMessage message={error} />
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<ListingFilters
search={search}
onSearchChange={setSearch}
dates={dates}
onDatesChange={setDates}
guests={guests}
onGuestsChange={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}
export default HomePage;Complete Refactored Code
Here's the complete refactored HomePage:
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/state/slices/listingsSlice';
import ListingList from '@/components/ListingList';
import ListingFilters from '@/components/ListingFilters';
import Spinner from '@/components/Spinner';
import ErrorMessage from '@/components/ErrorMessage';
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
const [search, setSearch] = useState('');
const [dates, setDates] = useState({ from: null, to: null });
const [guests, setGuests] = useState(1);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);
const filteredListings = useMemo(() => {
return items.filter((listing) => {
const matchesSearch = listing.title
.toLowerCase()
.includes(search.toLowerCase());
const matchesGuests = listing.maxGuests >= guests;
const matchesDates = true;
return matchesSearch && matchesGuests && matchesDates;
});
}, [items, search, guests]);
if (status === 'loading') {
return (
<div className="flex justify-center items-center min-h-screen">
<Spinner />
</div>
);
}
if (status === 'failed') {
return (
<div className="container mx-auto px-4 py-8">
<ErrorMessage message={error} />
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<ListingFilters
search={search}
onSearchChange={setSearch}
dates={dates}
onDatesChange={setDates}
guests={guests}
onGuestsChange={setGuests}
/>
<ListingList listings={filteredListings} />
</div>
);
}
export default HomePage;Before vs After
function HomePage() {
const { data: listings, isLoading, error } = useFetch('/listings');
const [search, setSearch] = useState('');
const filteredListings = useMemo(() => {
if (!listings) return [];
return listings.filter(/* ... */);
}, [listings, search]);
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
return <ListingList listings={filteredListings} />;
}Issues:
- Data lives only in
HomePage - Other components can't access listings
- Duplicate fetches if multiple components need data
function HomePage() {
const dispatch = useDispatch();
const { items, status, error } = useSelector((state) => state.listings);
const [search, setSearch] = useState('');
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);
const filteredListings = useMemo(() => {
return items.filter(/* ... */);
}, [items, search]);
if (status === 'loading') return <Spinner />;
if (status === 'failed') return <ErrorMessage message={error} />;
return <ListingList listings={filteredListings} />;
}Benefits:
- Data stored in Redux
- Any component can access listings
- Single source of truth
- Better caching
What we gained:
✅ Shared State
- Other pages can read
state.listings.items - No duplicate API calls
✅ Better Structure
- Clear separation: Redux (global) vs local (UI)
- Easier to test
✅ Redux DevTools
- See all state changes
- Time-travel debugging
- Action history
✅ Scalability
- Easy to add more features
- Favorites, cart, etc. follow same pattern
✅ Performance
- Redux caches data
- Only fetches once
- Efficient re-renders
Testing the Refactor
Open the App
Navigate to HomePage - you should see listings load!
Check Redux DevTools
Open Redux DevTools - you should see:
Actions:
listings/fetchListings/pendinglistings/fetchListings/fulfilled
State:
{
"listings": {
"items": [/* listings */],
"favorites": [],
"status": "succeeded",
"error": null
}
}Test Filtering
Type in search box - filtering should work as before!
The difference? Now listings data is in Redux! 🎉
Common Issues
Issue: "Cannot read property 'items' of undefined"
Problem: Trying to access state before store is set up
Solution: Make sure Provider wraps your app:
// In App.jsx
<Provider store={store}>
<Router />
</Provider>Issue: Infinite Loop of Fetches
Problem: Missing status check in useEffect
// ❌ BAD
useEffect(() => {
dispatch(fetchListings()); // Runs forever!
}, [dispatch]);Solution: Check status:
// ✅ GOOD
useEffect(() => {
if (status === 'idle') {
dispatch(fetchListings());
}
}, [status, dispatch]);What's Next?
Perfect! HomePage is now using Redux. In the next lesson, we'll:
- Add favorites feature - Already have
toggleFavoritereducer! - Test favorites - Make sure state updates correctly
- Prepare for UI - Get ready to show favorites
✅ Lesson Complete! HomePage now uses Redux for listings data! Other components can access the same data without duplicate fetches!
Key Takeaways
- ✅ Use
useSelectorto read from Redux store - ✅ Use
useDispatchto dispatch actions - ✅ Check
status === 'idle'before fetching - ✅ Keep UI-specific state local (search, filters)
- ✅ Move shared data to Redux (listings, favorites)
- ✅ Redux provides single source of truth
- ✅ Benefits: caching, shared state, DevTools