L3: Adding State to Filters
Make filters interactive with useState and controlled inputs
Our ListingFilters component looks great, but it's not interactive yet. In this lesson, we'll add state and event handlers to make the filters respond to user input.
What Are Controlled Components?
In React, a controlled component is an input element whose value is controlled by React state:
// ❌ Uncontrolled: React doesn't know the value
<input type="text" />
// ✅ Controlled: React controls the value
const [value, setValue] = useState('');
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>Benefits of Controlled Components:
- React always knows the current value
- Easy to validate or format input
- Can reset or pre-fill values programmatically
- Single source of truth (state)
Rule of Thumb: In React, always use controlled inputs for forms and user input.
Understanding Event Handlers
Event handlers are functions that run when users interact with your UI:
// Text input changes
<input onChange={(e) => handleChange(e)} />
// Button clicks
<button onClick={() => handleClick()} />
// Form submission
<form onSubmit={(e) => handleSubmit(e)} />
// Focus events
<input onFocus={() => handleFocus()} />
<input onBlur={() => handleBlur()} />Event Object (e):
e.target→ The element that triggered the evente.target.value→ Current value of inpute.preventDefault()→ Prevent default behavior
Step-by-Step Implementation
Step 1: Add useState Imports and State Variables
Open src/components/ListingFilters.jsx and add state for all filters:
import { useState } from 'react';
export function ListingFilters() {
// State for search input
const [search, setSearch] = useState('');
// State for date inputs
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
// State for guest counter
const [guests, setGuests] = useState(1);
return (
// ... JSX
);
}State Initialization:
search: Empty string (no search query yet)checkIn/checkOut: Empty strings (no dates selected)guests: Number 1 (minimum guests)
Step 2: Make Search Input Controlled
Connect the search input to state:
<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) => setSearch(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>How It Works:
- User types in the input
onChangeevent firessetSearchupdates state withe.target.value- Component re-renders
- Input displays new
valuefrom state
Step 3: Make Date Inputs Controlled
Connect check-in and check-out inputs to state:
<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) => setCheckIn(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) => setCheckOut(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>Date Format: The browser's date picker automatically formats dates as
YYYY-MM-DD(e.g., "2024-12-25").
Step 4: Add Guest Counter Event Handlers
Create functions to handle increment and decrement:
export function ListingFilters() {
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
// Handle decrement guest count
const handleDecrement = () => {
if (guests > 1) {
setGuests(guests - 1);
}
};
// Handle increment guest count
const handleIncrement = () => {
setGuests(guests + 1);
};
return (
// ... JSX
);
}Validation: The decrement function checks if
guests > 1to prevent going below 1 guest.
Step 5: Connect Buttons to Event Handlers
Wire up the guest counter buttons:
<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> // [!code highlight]
<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>Dynamic Display: The guest count now displays
{guests}from state instead of the hardcoded "1".
Step 6: Add Debug Display (Optional)
Temporarily display state values to see changes in real-time:
export function ListingFilters() {
// ... state and handlers
return (
<div className="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 className="text-xl font-semibold mb-4">Filter Stays</h2>
{/* Temporary Debug Display */}
<div className="bg-gray-100 p-4 rounded mb-4 text-sm"> // [!code ++]
<p><strong>Search:</strong> {search || '(empty)'}</p> // [!code ++]
<p><strong>Check-in:</strong> {checkIn || '(not set)'}</p> // [!code ++]
<p><strong>Check-out:</strong> {checkOut || '(not set)'}</p> // [!code ++]
<p><strong>Guests:</strong> {guests}</p> // [!code ++]
</div> // [!code ++]
{/* Rest of the filters... */}
</div>
);
}Debugging Tip: This helps you verify that state is updating correctly. Remove it once everything works!
Step 7: Test in Browser
Save and test each filter:
- ✅ Type in search box → See search value update
- ✅ Select check-in date → See date update
- ✅ Select check-out date → See date update
- ✅ Click + button → Guest count increases
- ✅ Click − button → Guest count decreases (stops at 1)
Working! Your filters are now fully interactive and controlled by React state.
Complete ListingFilters Code
Here's the complete, interactive component:
import { useState } from 'react';
export function ListingFilters() {
// State management
const [search, setSearch] = useState('');
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guests, setGuests] = useState(1);
// Event handlers
const handleDecrement = () => {
if (guests > 1) {
setGuests(guests - 1);
}
};
const handleIncrement = () => {
setGuests(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) => setSearch(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) => setCheckIn(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) => setCheckOut(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>
);
}Understanding the Data Flow
Here's how controlled inputs work:
// 1. Initial render: input shows '' (empty)
const [search, setSearch] = useState('');
// 2. User types 'M' in input
<input
value={search} // Currently: ''
onChange={(e) => setSearch(e.target.value)}
/>
// 3. onChange fires with e.target.value = 'M'
setSearch('M')
// 4. State updates, component re-renders
// search is now 'M'
// 5. Input displays new value
<input value={search} /> // Now shows 'M'This creates a feedback loop:
- User input → Event
- Event → Update state
- State change → Re-render
- Re-render → Update input
- Repeat
Event Handler Patterns
Pattern 1: Inline Arrow Function
<input onChange={(e) => setSearch(e.target.value)} />Use when: Simple, one-line updates
Pattern 2: Named Function
const handleSearchChange = (e) => {
setSearch(e.target.value);
};
<input onChange={handleSearchChange} />Use when: Need to add validation or logging
Pattern 3: Named Function with Logic
const handleDecrement = () => {
if (guests > 1) {
setGuests(guests - 1);
}
};
<button onClick={handleDecrement}>−</button>Use when: Need conditional logic or multiple operations
🎯 Handler Naming Convention
You'll notice we use two naming patterns for event handlers. This is a React community convention:
Internal Handlers: handle*
Functions defined inside your component use handle*:
function SearchBar() {
const handleClick = () => { ... };
const handleSubmit = () => { ... };
const handleChange = () => { ... };
return <button onClick={handleClick}>Search</button>;
}Prop Callbacks: on*
Functions passed as props from parent use on*:
function SearchBar({ onSearchChange, onSubmit }) {
return (
<form onSubmit={onSubmit}>
<input onChange={(e) => onSearchChange(e.target.value)} />
</form>
);
}
// Parent component:
function HomePage() {
const handleSearchChange = (value) => {
console.log('Search:', value);
};
return <SearchBar onSearchChange={handleSearchChange} />;
}Why this pattern?
handle*= "I handle this internally"on*= "Call me when this happens" (callback prop)
This convention helps you immediately understand:
- Where the function is defined (internal vs passed as prop)
- The flow of data and events in your component tree
Note: This is a convention, not a strict rule. The code works either way, but following conventions makes your code more readable to other React developers.
Common Mistakes to Avoid
❌ Mistake 1: Forgetting value Prop
// Wrong: Input is uncontrolled
<input onChange={(e) => setSearch(e.target.value)} />
// Correct: Input is controlled
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
/>❌ Mistake 2: Calling Function Instead of Passing It
// Wrong: Calls handleIncrement immediately on render
<button onClick={handleIncrement()}>+</button>
// Correct: Passes function reference
<button onClick={handleIncrement}>+</button>❌ Mistake 3: Using Wrong Event Property
// Wrong: e.value doesn't exist
onChange={(e) => setSearch(e.value)}
// Correct: e.target.value
onChange={(e) => setSearch(e.target.value)}❌ Mistake 4: Mutating State
// Wrong: Mutating state directly
const handleIncrement = () => {
guests = guests + 1; // ❌
};
// Correct: Using setter function
const handleIncrement = () => {
setGuests(guests + 1); // ✅
};Advanced: Validation and Constraints
You can add validation in event handlers:
// Limit guest count to max 10
const handleIncrement = () => {
if (guests < 10) {
setGuests(guests + 1);
}
};
// Prevent check-out before check-in
const handleCheckOutChange = (e) => {
const newCheckOut = e.target.value;
if (checkIn && newCheckOut < checkIn) {
alert('Check-out must be after check-in');
return;
}
setCheckOut(newCheckOut);
};
// Sanitize search input
const handleSearchChange = (e) => {
const sanitized = e.target.value.trim();
setSearch(sanitized);
};Checkpoint
What's Next?
Our filters are now interactive, but they only work inside the ListingFilters component. The HomePage doesn't know about the filter values!
In Lesson 4, we'll learn state lifting - a crucial pattern for sharing state between components. We'll:
- Move filter state to HomePage
- Pass values down as props
- Pass updater functions as callbacks
- Enable HomePage to use filter values
This is where React's component communication really shines! 🚀