L4: Lifting State Up
Learn how to share state between components using props and callbacks
Right now, the filter state lives in ListingFilters, but HomePage needs access to these values to filter the listings. The solution? Lifting state up - moving state to the nearest common ancestor.
The Problem: Isolated State
Currently, our component tree looks like this:
HomePage
├── listings (state ✅)
└── ListingFilters
├── search (state ❌ - isolated!)
├── checkIn (state ❌ - isolated!)
├── checkOut (state ❌ - isolated!)
└── guests (state ❌ - isolated!)Problem: HomePage can't access the filter values because they're trapped in ListingFilters.
The Solution: Lift State Up
Move the filter state to HomePage (the common ancestor):
HomePage
├── listings (state ✅)
├── search (state ✅)
├── checkIn (state ✅)
├── checkOut (state ✅)
├── guests (state ✅)
└── ListingFilters
├── search (prop from parent)
├── checkIn (prop from parent)
├── checkOut (prop from parent)
└── guests (prop from parent)Solution: State lives in HomePage, gets passed down as props to ListingFilters.
Key Principle: When two components need to share state, move the state to their closest common ancestor.
Understanding Lifting State Up
Data Flow Pattern
function Parent() {
const [value, setValue] = useState('');
return (
<Child
value={value} // Pass state down ⬇️
onChange={setValue} // Pass updater down ⬇️
/>
);
}
function Child({ value, onChange }) {
return (
<input
value={value} // Use prop as value
onChange={(e) => onChange(e.target.value)} // Call parent's updater ⬆️
/>
);
}Flow:
- Parent owns state
- Child receives value via props
- Child receives updater function via props
- Child calls updater when user interacts
- State updates in parent
- New value flows down to child
Step-by-Step Implementation
Step 1: Move State to HomePage
Cut the filter state from ListingFilters and paste it into HomePage:
import { useState } from 'react';
import PropertyCard from '../components/PropertyCard';
import { ListingFilters } from '../components/ListingFilters';
export function HomePage() {
// Listings state
const [listings, setListings] = useState([
// ... your listings data
]);
// Filter state (moved from ListingFilters)
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Available Stays</h1>
<ListingFilters />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{listings.map(listing => (
<PropertyCard key={listing.id} listing={listing} />
))}
</div>
</div>
);
}State is now in HomePage! The parent component controls all filter values.
Step 2: Pass State as Props to ListingFilters
Pass the filter values down to ListingFilters:
export function HomePage() {
const [listings, setListings] = useState([/* ... */]);
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Available Stays</h1>
<ListingFilters
search={search}
checkIn={checkIn}
checkOut={checkOut}
guests={guests}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{listings.map(listing => (
<PropertyCard key={listing.id} listing={listing} />
))}
</div>
</div>
);
}Props Passed Down: Filter values now flow from parent to child.
Step 3: Pass Updater Functions as Props
Pass the setter functions so ListingFilters can update the values:
<ListingFilters
search={search}
checkIn={checkIn}
checkOut={checkOut}
guests={guests}
onSearchChange={setSearch}
onCheckInChange={setCheckIn}
onCheckOutChange={setCheckOut}
onGuestsChange={setGuests}
/>Naming Convention: Use
on[Action]for callback props (e.g.,onSearchChange,onGuestsChange).
Step 4: Update ListingFilters to Accept Props
Modify ListingFilters to use props instead of local state:
import { useState } from 'react';
export function ListingFilters({
search,
checkIn,
checkOut,
guests,
onSearchChange,
onCheckInChange,
onCheckOutChange,
onGuestsChange,
}) {
// Remove local state
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
// Update event handlers to use props
const handleDecrement = () => {
if (guests > 1) {
setGuests(guests - 1);
onGuestsChange(guests - 1);
}
};
const handleIncrement = () => {
setGuests(guests + 1);
onGuestsChange(guests + 1);
};
return (
// ... JSX (update inputs next)
);
}Props Instead of State: Component now receives values and updaters from parent.
Step 5: Update Input onChange Handlers
Connect inputs to the callback props:
<input
id="search"
type="text"
placeholder="e.g., Malibu, Beach House..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => onSearchChange(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>Update all three inputs:
{/* Search Input */}
<input
value={search}
onChange={(e) => onSearchChange(e.target.value)}
/>
{/* Check-in Date */}
<input
type="date"
value={checkIn}
onChange={(e) => onCheckInChange(e.target.value)}
/>
{/* Check-out Date */}
<input
type="date"
value={checkOut}
onChange={(e) => onCheckOutChange(e.target.value)}
/>Calling Parent Functions: When users interact with inputs, we call the parent's setter functions via props.
Step 6: Test the Lifted State
Add a debug display in HomePage to verify state is updating:
export function HomePage() {
const [listings, setListings] = useState([/* ... */]);
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Available Stays</h1>
{/* Debug Display */}
<div className="bg-blue-50 p-4 rounded mb-4"> // [!code ++]
<p className="font-bold">HomePage can see filters:</p> // [!code ++]
<p>Search: {search || '(empty)'}</p> // [!code ++]
<p>Check-in: {checkIn || '(not set)'}</p> // [!code ++]
<p>Check-out: {checkOut || '(not set)'}</p> // [!code ++]
<p>Guests: {guests}</p> // [!code ++]
</div> // [!code ++]
<ListingFilters
search={search}
checkIn={checkIn}
checkOut={checkOut}
guests={guests}
onSearchChange={setSearch}
onCheckInChange={setCheckIn}
onCheckOutChange={setCheckOut}
onGuestsChange={setGuests}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{listings.map(listing => (
<PropertyCard key={listing.id} listing={listing} />
))}
</div>
</div>
);
}Test: Type in the search box, change dates, click guest buttons. The HomePage should display the updated values!
Success! State is managed in HomePage but controlled in ListingFilters.
Complete Code
HomePage (Parent - Owns State)
import { useState } from 'react';
import PropertyCard from '../components/PropertyCard';
import { ListingFilters } from '../components/ListingFilters';
export function HomePage() {
// Listings state
const [listings, setListings] = useState([
{
id: 1,
title: "Cozy Beach House",
price: 250,
location: "Malibu, CA",
image: "/images/beach-house.jpg",
guests: 4,
bedrooms: 2,
bathrooms: 2,
},
{
id: 2,
title: "Mountain Cabin",
price: 180,
location: "Aspen, CO",
image: "/images/cabin.jpg",
guests: 6,
bedrooms: 3,
bathrooms: 2,
},
{
id: 3,
title: "Downtown Loft",
price: 320,
location: "New York, NY",
image: "/images/loft.jpg",
guests: 2,
bedrooms: 1,
bathrooms: 1,
},
]);
// Filter state (lifted up from ListingFilters)
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Available Stays</h1>
<ListingFilters
search={search}
checkIn={checkIn}
checkOut={checkOut}
guests={guests}
onSearchChange={setSearch}
onCheckInChange={setCheckIn}
onCheckOutChange={setCheckOut}
onGuestsChange={setGuests}
/> // [!code highlight]
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{listings.map(listing => (
<PropertyCard key={listing.id} listing={listing} />
))}
</div>
</div>
);
}ListingFilters (Child - Uses Props)
export function ListingFilters({
search,
checkIn,
checkOut,
guests,
onSearchChange,
onCheckInChange,
onCheckOutChange,
onGuestsChange,
}) {
const handleDecrement = () => {
if (guests > 1) {
onGuestsChange(guests - 1);
}
};
const handleIncrement = () => {
onGuestsChange(guests + 1);
};
return (
<div className="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 className="text-xl font-semibold mb-4">Filter Stays</h2>
{/* Search Input */}
<div className="mb-4">
<label htmlFor="search" className="block text-sm font-medium text-gray-700 mb-2">
Search by location
</label>
<input
id="search"
type="text"
placeholder="e.g., Malibu, Beach House..."
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Date Inputs */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label htmlFor="checkin" className="block text-sm font-medium text-gray-700 mb-2">
Check-in
</label>
<input
id="checkin"
type="date"
value={checkIn}
onChange={(e) => onCheckInChange(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="checkout" className="block text-sm font-medium text-gray-700 mb-2">
Check-out
</label>
<input
id="checkout"
type="date"
value={checkOut}
onChange={(e) => onCheckOutChange(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
{/* Guest Counter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Number of guests
</label>
<div className="flex items-center gap-4">
<button
onClick={handleDecrement}
className="w-10 h-10 rounded-full bg-gray-200 hover:bg-gray-300 flex items-center justify-center font-bold"
>
−
</button>
<span className="text-lg font-semibold">{guests}</span>
<button
onClick={handleIncrement}
className="w-10 h-10 rounded-full bg-blue-500 hover:bg-blue-600 text-white flex items-center justify-center font-bold"
>
+
</button>
</div>
</div>
</div>
);
}Visual: State Flow Diagram
When to Lift State Up
Lift state up when:
✅ Multiple components need the same data
// Both need user data
<Profile user={user} />
<Settings user={user} />✅ Parent needs to react to child's state changes
// Parent filters list based on child's input
<SearchBar onSearch={handleSearch} />
<ResultsList results={filtered} />✅ Sibling components need to communicate
// Siblings need to share state
<FilterBar filters={filters} onChange={setFilters} />
<ProductGrid filters={filters} />Common Mistakes
❌ Mistake 1: Passing State Value But Not Updater
// Wrong: Child can't update
<Child value={value} />
// Correct: Child can read and update
<Child value={value} onChange={setValue} />❌ Mistake 2: Directly Mutating Props
// Wrong: Can't mutate props
function Child({ value }) {
value = 'new value'; // ❌
}
// Correct: Call callback to update
function Child({ value, onChange }) {
onChange('new value'); // ✅
}❌ Mistake 3: Keeping Duplicate State
// Wrong: State in both components
function Parent() {
const [value, setValue] = useState('');
return <Child />;
}
function Child() {
const [value, setValue] = useState(''); // ❌ Duplicate!
}
// Correct: State only in parent
function Parent() {
const [value, setValue] = useState('');
return <Child value={value} onChange={setValue} />;
}Checkpoint
What's Next?
Now HomePage can see all filter values, but we're not using them yet!
In Lesson 5, we'll:
- Implement the filtering logic
- Filter listings by search term
- Filter by dates and guest count
- Show results in real-time
- Handle empty states (no results)
Time to make these filters actually work! 🎯