Hello! Welcome back.
In our last lesson, you successfully added a powerful search bar to our product page. By combining useState with the .filter() and .some() array methods, you made it possible to search across multiple product fields. This was a significant step in making our e-commerce app interactive.
Today, we'll continue enhancing our product catalog by building on that foundation. Our goal is to implement a category filter to further refine the product listing. This will allow users to narrow down the product grid by selecting a specific category, and it will give you more practice with state management and the declarative array methods you want to master.
The Strategy: Dynamic Categories and Chained Filters
Our approach will involve four main steps:
- Extract Unique Categories: The API doesn't give us a separate list of categories. We'll need to dynamically generate a unique list from the
productsarray itself. - Manage Category State: We'll introduce a new piece of state using
useStateto keep track of which category the user has selected. - Render Filter UI: We'll use our dynamic list of categories to render a set of buttons that users can click to filter the products.
- Combine Filters: We will update our filtering logic to make the search bar and the new category filter work together seamlessly.
Let's get started.
1. Extracting Unique Categories from Data
First, we need a list of all available product categories. Since this list is derived from the products themselves, we can create it right inside our ProductsPage component.
A great way to do this in modern JavaScript involves a combination of Array.prototype.map and the Set object.
products.map(p => p.category)will create a new array containing the category of every product. This list will have many duplicates (e.g.,['electronics', 'jewelery', 'electronics', ...]).new Set(...)takes an iterable (like our array of categories) and creates aSetobject, which automatically stores only unique values.[...new Set(...)]uses the spread syntax to convert theSetback into an array.
Let's implement this in src/pages/ProductsPage.jsx. We'll also add an "All" category to allow the user to see all products again.
// In src/pages/ProductsPage.jsx, inside the component function but before the return statement
// ... after your state declarations ...
// Dynamically get unique categories from the products list
const allCategories = ['All', ...new Set(products.map(product => product.category))];
// ... the rest of your component
Place this line just before your existing filteredProducts logic. Now, allCategories is an array like ['All', 'electronics', 'jewelery', "men's clothing", "women's clothing"] that will automatically update if the product data ever changes.
2. Managing Category State and UI
Next, we need a state variable to hold the currently selected category and a UI for the user to select one.
A. Add State for the Selected Category
In ProductsPage.jsx, add a new useState call near your other state variables. We'll default it to "All".
// ... inside the ProductsPage component function
const [query, setQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All'); // <-- ADD THIS LINE
B. Render the Category Buttons
Now, let's use the allCategories array we created to render a button for each category. We can place these buttons between the search bar and the product grid.
// ... inside the ProductsPage component's return statement ...
<input /* ... your existing search input ... */ />
{/* Add the category filter buttons */}
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{allCategories.map(category => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
style={{
padding: '0.5rem 1rem',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer',
// Highlight the active button
backgroundColor: selectedCategory === category ? '#007bff' : '#f8f9fa',
color: selectedCategory === category ? 'white' : 'black'
}}
>
{category}
</button>
))}
</div>
{loading && <p>Loading products...</p>}
{/* ... rest of the JSX */}
Here's what this code does:
- We map over our
allCategoriesarray. - For each category, we render a
<button>. Thekeyis the category name itself, as it's unique in this list. - The
onClickhandler updates ourselectedCategorystate to the category that was just clicked. - The
styleattribute includes a ternary operator(selectedCategory === category ? ... : ...)to apply different styles to the active button, providing clear visual feedback to the user.
Run your app now. You should see a row of clickable category buttons. Clicking them will change which one is highlighted, but the product list won't change yet. Let's fix that.
3. Combining the Search and Category Filters
This is where we bring everything together. We need to filter our product list by both the selected category and the search query. A clean and efficient way to do this is to chain multiple .filter() calls together.
The first filter will handle the category, and the second will handle the search query. This works because the second .filter() will only operate on the items that passed through the first one.
Update the filtering logic in ProductsPage.jsx:
// ... inside the ProductsPage component, replace your old filtering logic with this
const searchableKeys = ['title', 'description', 'category'];
const filteredProducts = products
// 1. Filter by selected category first
.filter(product => {
// If 'All' is selected, don't filter by category (return true for all products)
if (selectedCategory === 'All') {
return true;
}
// Otherwise, only keep the products that match the selected category
return product.category === selectedCategory;
})
// 2. Then, filter by the search query on the remaining products
.filter(product => {
// Return true if at least one of the keys' values includes the query
return searchableKeys.some(key =>
product[key].toLowerCase().includes(query.toLowerCase())
);
});
// The rest of your component remains the same (the return statement uses filteredProducts)
With this change, your component's rendering logic now follows this sequence on every state update:
- It takes the full
productslist. - It filters them down based on the
selectedCategory. - It takes that already-filtered list and filters it again based on the
query. - Finally, it maps over the result to render the
ProductCardcomponents.
Now, run the app. You should have a fully functional product page where you can select a category, and then use the search bar to search for items within that category.
Conclusion
Excellent work! You have now implemented a second layer of filtering, making the product catalog significantly more user-friendly. In doing so, you've gained more practical experience with some of the most important concepts in React and modern JavaScript.
Key Takeaways:
- Dynamic UI from Data: You learned to derive UI elements (category buttons) directly from your data, making the component flexible and maintainable.
- ES6 for Data Manipulation: You used
new Set()to efficiently extract unique values from an array, a common and powerful pattern. - Composing State: You managed multiple, independent pieces of state (
queryandselectedCategory) to control the UI. - Chaining Array Methods: You chained two
.filter()calls to apply multiple, sequential filtering criteria in a clean and declarative way. This is a fundamental technique for complex data transformations.
Preview of the Next Lesson:
Our catalog page is now quite powerful, but the products themselves are just cards. The next step is to allow users to click on a product to see more details. In the next lesson, we will begin the next phase of our project by creating a dynamic route for a product detail page. This will involve using React Router to navigate to a unique URL (like /products/1) for each product and fetching the specific data for that item.