Introduction: The Role of a Book Details Page

Welcome back! So far, you have learned how to fetch, sort, and paginate a list of books in your catalog app. In this lesson, you will take the next step by building a Book Details Page. This page allows users to click on a book in the catalog and see more information about it, such as the title, author, and some interesting statistics.

A details page is a key part of any data catalog. It helps users dive deeper into a specific item and learn more about it. By the end of this lesson, you will know how to fetch and display detailed information for a single book and how to connect your catalog to this new page.

Routing: enabling books/:id

Let’s start by declaring a dynamic route so the app can open a details page for any book. A dynamic segment (:id) captures the book identifier directly from the URL (e.g., /books/2).

Here’s how your routing is set up in src/routes/router.tsx:

import BookDetailsPage from "../features/book/BookDetailsPage";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    children: [
      { index: true, element: <HomePage /> },
      { path: "catalog", element: <CatalogPage /> },
      { path: "books/:id", element: <BookDetailsPage /> }, // ← new dynamic route
      // ...other routes
    ],
  },
]);

Why this matters

  • books/:id tells React Router to match any id and pass it to the page via useParams().
  • Navigating to /books/2 renders BookDetailsPage with { id: "2" }.
  • Keeping details under /books/... keeps URLs predictable and deep-linkable from the catalog.
Backend contracts used by the details page

We’ll consume two public endpoints your backend already exposes:

GET /books/:id (Public)
- Purpose: fetch a single book by ID.
- Response: { success: true, data: Book }
- 404 if not found.
- Tested for id=2 (Dune): OK.

GET /books/:id/stats (Public)
- Purpose: aggregated reading stats for a book.
- Data fields: { bookId, readers, avgCurrentPage, completionRate, avgProgressPct }
- Example tested for book 2:
  {"success":true,"data":{"bookId":2,"readers":1,"avgCurrentPage":100,"completionRate":0,"avgProgressPct":0.24271844660194175}}

These two endpoints give us entity data (title/author/etc.) and derived metrics (how folks are progressing). We’ll fetch them in parallel and render the combined view.

Fetching Book Details and Stats

To show detailed information about a book, you need to fetch it from the API using its ID. You will also fetch some statistics about the book, like how many users have it on their shelf and the average reading progress.

Here are the two functions you will use, found in src/api/books.ts:

import { apiClient } from "./client";
import { Book, BookStats } from "../lib/types";

// ...rest of the functions

// Fetch a single book by its ID
export const getBookById = async (id: number | string): Promise<Book> => {
  const res = await apiClient.get<any>(`/books/${id}`);
  return res.data.data as Book;
};

// Fetch statistics for a single book
export const getBookStats = async (id: number | string): Promise<BookStats> => {
  const res = await apiClient.get<any>(`/books/${id}/stats`);
  return res.data.data as BookStats;
};

What this does

  • getBookById(id) → GET /books/:id, returns the Book object. If the server returns 404, React Query will surface an error you can handle in the UI.
  • getBookStats(id) → GET /books/:id/stats, returns aggregates:
    • readers: number of users tracking this book.
    • avgCurrentPage: average page currently reached.
    • completionRate: fraction of readers who finished (0..1).
    • avgProgressPct: average progress (0..1). Multiply by 100 for a percentage.
  • Both helpers unwrap res.data so components don’t care about envelopes and can consume strongly-typed data.
Building the Book Details Page Component

Now, let’s put it all together in the BookDetailsPage component. This component will:

  • Get the book ID from the URL,
  • Fetch the book’s details and stats,
  • Show a loading spinner while data is loading,
  • Display the book’s information and statistics.

Here is the code for src/features/book/BookDetailsPage.tsx:

import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { getBookById, getBookStats } from "../../api/books";
import Spinner from "../../components/Spinner";

export default function BookDetailsPage() {
  const { id } = useParams<{ id: string }>();

  const { data: book, isLoading: isLoadingBook } = useQuery({
    queryKey: ["book", id],
    queryFn: () => getBookById(id!),
    enabled: !!id,
  });

  const { data: stats, isLoading: isLoadingStats } = useQuery({
    queryKey: ["bookStats", id],
    queryFn: () => getBookStats(id!),
    enabled: !!id,
  });

  if (isLoadingBook || isLoadingStats) return <Spinner />;
  if (!book) return <p>Book not found.</p>;

  return (
    <div>
      <h1 className="text-4xl font-bold">{book.title}</h1>
      <p className="text-xl text-slate-400 mt-2">by {book.author}</p>
      <div className="mt-6 p-4 bg-slate-800 rounded-lg">
        <h2 className="text-2xl font-bold">Stats</h2>
        <p>Readers: {stats?.readers ?? 0}</p>
        <p>Average Progress: {stats ? Math.round((stats.avgProgressPct || 0) * 100) : 0}%</p>
      </div>
    </div>
  );
}

Key ideas

  • useParams() provides { id } from the dynamic route.
  • Two independent queries:
    • ["book", id] fetches the primary entity.
    • ["bookStats", id] fetches aggregates. They run in parallel and revalidate independently.
  • enabled: !!id guards against running queries before the router provides an id.
  • Loading & errors:
    • While either request loads → show <Spinner />.
    • If the book request fails (including 404) → show a clear “not found” message.
    • If stats fail but the book loads → render book details and a gentle stats fallback.
  • Percentages are derived from fractions (0.1) and rounded for readability.

Example Output:

When you visit /books/123, you might see:

The Great Gatsby
by F. Scott Fitzgerald

Stats
On 42 shelves
Average Progress: 67.5%
Linking From the Catalog to the Details Page

Finally, ensure cards in the catalog link to the details page. This makes discovery natural: click a card → land on /books/:id. To let users open the Book Details Page, you need to link each book in the catalog to its details page. This is done in the BookCard component using React Router’s Link.

Here’s the code from src/features/catalog/BookCard.tsx:

import { Link } from "react-router-dom";

type Props = {
  id: string;
  title: string;
  author: string;
};

function BookCard({ id, title, author }: Props) {
  return (
    <Link to={`/books/${id}`} className="block bg-slate-800 rounded-lg p-4 shadow-md hover:bg-slate-700 transition-colors">
      <div className="flex flex-col h-full">
        <h3 className="text-lg font-bold text-sky-400">{title}</h3>
        <p className="text-slate-400 mt-1">by {author}</p>
      </div>
    </Link>
  );
}

export default BookCard;

Why this is effective

  • The Link composes a semantic, accessible navigation target.
  • Deep-linking: You can share /books/2 directly and the details screen loads the correct content.
Summary And Practice Preview

In this lesson, you learned how to build a Book Details Page that shows more information about a single book. You reviewed how dynamic routing works, how to fetch a book’s details and stats from the API, and how to display this information in a user-friendly way. You also saw how to link from the catalog to the details page so users can explore books in more depth.

Next, you’ll get a chance to practice these skills by building and using the Book Details Page yourself. This hands-on practice will help you reinforce what you’ve learned and prepare you for even more advanced features in the future. Good luck!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal