Code To Learn logo

Code To Learn

M2: State & Events

L4: Lifting State Up

Learn how to share state between components using props and callbacks

State Management Props Component Communication

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:

Current State Location
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):

After Lifting State Up
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

Parent-Child Communication
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:

  1. Parent owns state
  2. Child receives value via props
  3. Child receives updater function via props
  4. Child calls updater when user interacts
  5. State updates in parent
  6. 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:

src/pages/HomePage.jsx
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:

src/pages/HomePage.jsx
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:

src/pages/HomePage.jsx
<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:

src/components/ListingFilters.jsx
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:

src/components/ListingFilters.jsx
<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:

src/components/ListingFilters.jsx
{/* 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:

src/pages/HomePage.jsx
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)

src/pages/HomePage.jsx
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)

src/components/ListingFilters.jsx
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

HomePage.jsx
ListingFilters.jsx

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! 🎯