Plan, build and test a filterable catalog with React, then publish it as a portfolio project.
Why it matters
Employers hiring junior front-end and mobile developers look for evidence that you can build a small but complete feature: data, components, state, user input, an empty state and tests. A searchable catalog is a classic portfolio piece because it touches every topic in this course and is easy for a reviewer to try in a minute.
What you will build
A product (or book, course or recipe) catalog with:
- a list of items rendered from a data array, each in a reusable card component;
- a search box that filters by name as you type, ignoring case and extra spaces;
- a category dropdown that combines with the search;
- a result count and a friendly empty state ("No items match");
- at least two component tests with React Testing Library.
Plan the components
Sketch the screen and draw boxes around each piece, then name them:
App
├── SearchBar (controlled text input + category select)
├── ResultCount ("4 items")
└── ItemList
└── ItemCard (one per item)
Then decide where state lives. Only two values change: the search text and the selected category. Both affect the list, so they live in App (lifted up) and flow down as props. The filtered list and the count are derived, not stored.
Set up the project
You will run this on your own computer with Node.js installed (a current LTS version; the Vite docs list the minimum). Vite creates a React project in seconds:
npm create vite@latest catalog -- --template react
cd catalog
npm install
npm run dev
Open the local URL it prints. Edit src/App.jsx and watch it update.
Data and filtering
Put sample data in src/items.js:
export const items = [
{ id: 1, name: "Mechanical keyboard", category: "Hardware", price: 89 },
{ id: 2, name: "USB-C hub", category: "Hardware", price: 35 },
{ id: 3, name: "Code editor theme", category: "Software", price: 0 },
{ id: 4, name: "Monitor arm", category: "Hardware", price: 60 },
{ id: 5, name: "Note-taking app", category: "Software", price: 8 },
];
Write the filter as a plain, testable function:
export function filterItems(items, query, category) {
const needle = query.trim().toLowerCase();
return items.filter(
(item) =>
(category === "All" || item.category === category) &&
item.name.toLowerCase().includes(needle)
);
}
And use it in App:
import { useState } from "react";
export default function App() {
const [query, setQuery] = useState("");
const [category, setCategory] = useState("All");
const visible = filterItems(items, query, category);
return (
<main>
<h1>Catalog</h1>
<SearchBar
query={query}
onQueryChange={setQuery}
category={category}
onCategoryChange={setCategory}
/>
<p role="status">{visible.length} item(s)</p>
{visible.length === 0 ? (
<p>No items match.</p>
) : (
<ItemList items={visible} />
)}
</main>
);
}
SearchBar holds no state of its own: it renders an <input> labelled "Search" and a labelled <select> and calls the change functions it receives.
Add tests
Install the testing tools:
npm install -D vitest jsdom @testing-library/react @testing-library/user-event
Add "test": "vitest" to the scripts in package.json, and in vite.config.js add test: { environment: "jsdom", globals: true } to the config object (globals lets Testing Library clean up the DOM after each test). A first test:
// src/App.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { test, expect } from "vitest";
import App from "./App";
test("filters items as you type", async () => {
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByRole("textbox", { name: /search/i }), "usb");
expect(screen.getByText("USB-C hub")).toBeTruthy();
expect(screen.queryByText("Monitor arm")).toBeNull();
});
Add a second test for the empty state, and plain unit tests for filterItems (empty query, mixed case, category plus query).
Make it portfolio-ready
- Write a short README: what it does, how to run it (
npm install,npm run dev,npm test), and what you would add next. - Commit as you go with clear messages, and push to a public GitHub repository.
- Optionally deploy the free static build (
npm run buildproducesdist/) to a free hosting tier such as Netlify, Vercel or GitHub Pages, and link it from the README. Free tiers do not need paid add-ons for a static site.
Stretch goals
- Sort by price or name (sort a copy).
- Keep the search in the URL (
?q=usb) so results are shareable. - Load the items from a JSON file with
fetchin an effect, with loading and error states. - Extract the filtering logic into a
useFilterhook.
Common mistakes
- Storing the filtered list in state and forgetting to update it.
- Inputs without labels, which also makes them hard to find in tests.
- Keys from the array index, which break when the list is filtered.
- Tests that only check that the page renders.
Recap
- Plan components and decide where state lives before coding.
- Keep minimal state (query, category) and derive the visible list and count.
- Test behaviour the way a user experiences it, including the empty state.
- Ship with a README, commits and, optionally, a free deployment.
Try it
Open the Do tab for the step-by-step build checklist.