Project 4

Project 4
Book Search with Routing and Debounced Search
A search results page and a separate detail page, navigated with React Router, with search-as-you-type that doesn't hammer the API
Difficulty: Advanced Introduces: React Router, debouncing

This project reaches slightly ahead of where the chapter-by-chapter courses are at — it needs two ideas that don't have their own Fundamentals chapter yet: routing (multiple "pages" in a single-page app) and debouncing (delaying a search request until typing actually pauses). Both get a working primer below; React Router gets its proper full chapter in Intermediate Chapter 5, and this project is a hands-on first taste of it ahead of that.

Skills This Project Exercises
React Router basics URL params (useParams) Debounced search useEffect cleanup Loading & error state List + detail view
React Router — the minimum you need for this project
Install it with npm install react-router-dom. Four pieces cover this whole project: <BrowserRouter> wraps the app once, at the top; <Routes>/<Route path="..." element={...}> define which component renders for which URL; <Link to="..."></Link> navigates without a full page reload; and useParams() reads a dynamic piece of the URL (like a book's id) inside the component that route renders.
<Routes> <Route path="/" element={<SearchPage />} /> <Route path="/book/:id" element={<BookDetailPage />} /> </Routes>
Debouncing — the minimum you need for this project
Firing a fetch on every single keystroke wastes requests and can show results out of order as old, slow responses arrive after newer ones. Debouncing waits for a short pause in typing (e.g. 400ms with nothing new typed) before actually firing the request — using setTimeout inside a useEffect, with the cleanup function (from Fundamentals Chapter 8) cancelling the pending timeout if the user types again before it fires.
useEffect(() => { const timeoutId = setTimeout(() => { // the actual fetch goes here, using the current query value }, 400); return () => clearTimeout(timeoutId); }, [query]);

Suggested Data Source

The Open Library Search API is free and needs no API key, similar to Open-Meteo in Project 2 — https://openlibrary.org/search.json?q=<query> returns a list of matching books, and https://openlibrary.org/works/<id>.json returns details for one specific book.

Requirements

  • A search page at / with a text input — typing should debounce before firing a search request, not fire on every keystroke.
  • Search results render as a list, each showing at least a title and author, and linking to a detail page for that specific book.
  • A detail page at a URL like /book/:id, fetching and showing more information about that one specific book based on the id in the URL.
  • The detail page includes a way to navigate back to the search page (a <Link>, not the browser's own back button only).
  • Both pages handle loading and error states properly, reusing the status-based pattern from Project 2.
  • Navigating between the two pages does not cause a full page reload — that's the whole point of using React Router instead of plain <a> tags.

Suggested Component Breakdown

App (BrowserRouter + Routes) ├── SearchPage // route "/" — owns query state + debounced results │ ├── SearchInput // controlled text input │ └── ResultsList // maps results to ResultItem, each wrapped in a Link │ └── ResultItem (×N) └── BookDetailPage // route "/book/:id" — reads id via useParams, fetches detail

A Reasonable Build Order

  1. Set up react-router-dom with two placeholder page components and a working <Link> between them, before any fetching exists at all — confirm navigation itself works first.
  2. Build the search input with a non-debounced fetch (firing on every keystroke) to confirm the API call and result rendering work correctly.
  3. Convert the search effect to the debounced version, confirming with the browser's network tab that requests are no longer firing on every single keystroke.
  4. Make each result item a <Link to={`/book/${id}`}>, carrying the right id into the URL.
  5. Build BookDetailPage, reading the id with useParams() and fetching that specific book's details in a useEffect keyed on the id.
  6. Add loading/error handling to both pages last, once the core search → detail flow works end to end.
A stale request can still "win" the race
If the user types a new search before the previous request finishes, both requests are still in flight — and there's no strict guarantee the newer one resolves last. A query value captured in a ref, or an ignore-flag set in the effect's cleanup function, are both reasonable ways to discard a response that's no longer for the current search term. It's fine to skip this for a first pass and revisit it once the basic flow works.
Stretch Goals
  • Extract the debounce logic into a reusable useDebounce custom hook (another preview of Intermediate Chapter 4).
  • Add pagination or "load more" to the search results.
  • Persist a small "recently viewed books" list to localStorage, shown on the search page.
  • Show a loading skeleton instead of a plain "Loading..." message.
  • Add a 404-style fallback route for any URL that doesn't match either page.
This project has no single solution file — the requirements above and your own judgment are the spec. Compare notes against the suggested component breakdown once you have something working, not before.