Welcome back! In the previous lessons, you learned how to fetch and display book data in your React catalog app and how to let users sort the results. As your catalog grows, you might notice that loading and displaying all books at once can slow down your app and make it harder for users to find what they want.
This is where pagination comes in. Pagination means breaking up a large list of items into smaller, more manageable pages. Instead of loading hundreds or thousands of books at once, you only load a few at a time — just what the user needs to see. This makes your app faster and easier to use.
In this lesson, you will learn how to add server-side pagination to your catalog. This means the server will only send a small chunk of books for each page, and your frontend will let users move between pages.
With server-side pagination, the client (your React app) asks the server for just one page of data at a time. The server responds with only the books for that page, along with information about the total number of books and how many are shown per page.
This is usually done by sending query parameters like page, sortBy, and order in the API request. For example:
The server then responds with something like:
items: The books for the current page.total: The total number of books in the catalog.page: The current page number.pageSize: How many books are shown per page.
This way, your app only needs to handle a small set of books at a time, making it faster and more responsive.
Let’s look at how you can fetch paginated data and display it in your catalog. Here’s the updated code for your API call and catalog page:
Explanation:
Let’s look at getBooks. It constructs a query string using URLSearchParams so the server receives exactly the sorting, paging, and search options you specify.
- Purpose: Build
/books?...with any mix of:sortBy(e.g.,"title"or"author"),order("asc"or"desc"),page(page number, 1-based),q(free-text search),pageSize(how many items per page — a key focus in this lesson).
- How it’s built:
- Conditional object spreads
...(page ? { page: String(page) } : {})include a key only when a value is present. String(page)andString(pageSize)ensure values are strings, as required by the URL format.new URLSearchParams({...}).toString()encodes and joins the keys/values into a valid query string (e.g.,sortBy=title&order=asc&page=2&pageSize=5).
- Conditional object spreads
- Why
URLSearchParamsmatters: It safely URL-encodes special characters (like spaces inq), avoids manual&concatenation bugs, and guarantees predictable ordering and formatting. - Why emphasize
pageSize: SettingpageSizelets the server trim responses to just N items per page. Here we’ll request 5 in the UI to demonstrate predictable pagination math and navigation performance.
In the next section, we'll see how the frontend uses this.
Let’s walk through the key behaviors in CatalogPage and why they matter.
Explanation:
- A. Reading and writing URL params (deep linkable state)
useSearchParams()gives you a read/write handle to the query string.- We read
sortBy,order,page, andq. Defaults:sortBy="title",order="asc",page=1,q="". - Keeping state in the URL allows bookmarks, back/forward navigation, and shareable links (e.g.,
?q=clean+code&page=3&sortBy=author&order=desc).
- B. Let’s look at the search effect. It debounces typing and syncs input → URL after 300ms:
- What
const next = new URLSearchParams(prev);does:- It clones the existing query parameters (
prev) into a new, mutableURLSearchParamsinstance. - We then edit this copy: set or delete
q, and (re)applysortBy,order, andpage='1'. Returningnextreplaces the current URL with the new query string.
- It clones the existing query parameters (
- Why we reset
pageto'1': Changing the search term should show results from the first page, or the user might land on an empty page for the new filter. - The
return () => clearTimeout(id)cleanup:- If the user keeps typing, we cancel the previous scheduled update. This prevents excessive URL churn and redundant network requests.
- Dependency array
[input]:- The effect runs whenever the input value changes (not on sort or page changes). This isolates search behavior from other concerns.
- Debouncing here keeps the UI snappy and reduces server load.
- What
- C. Fetching a specific page with
useQuery(andpageSize: 5)- What happens:
- The cache key includes
sortBy,order,page,q, andpageSize: 5. Each unique combination maps to its own cached result. queryFncallsgetBookswithpageSize: 5, ensuring the server returns exactly 5 items per page (or fewer on the last page). This is central to predictable pagination.keepPreviousData: truekeeps showing the prior page’s data while the next page is loading — no flicker or jarring blank states. TheisFetchingflag drives a subtle loading hint next to the search field.
- D. Page navigation with
handlePageChange - What it does:
- Updates the URL to the requested page, preserving the current
sortBy,order, andq(only ifqexists). - Because the URL (and thus the
queryKey) changes, React Query refetches the appropriate page automatically.
- Updates the URL to the requested page, preserving the current
- E. Calculating
totalPagesfor the UI - What it means:
data.totalis the count of all matching books on the server (after filters).data.pageSizeis how many items the server returns per page (we asked for 5).Math.ceil(total / pageSize)rounds up to ensure partial pages still count.- If
dataisn’t available yet, default to0to avoid rendering pagination prematurely.
Putting it together: The URL is the single source of truth for q, sortBy, order, and page. The query consumes those values along with a fixed pageSize: 5, and the UI reflects both results and pagination controls consistently.
Let’s look at the Pagination component. It receives currentPage, totalPages, and a callback:
Explanation:
- Rendering strategy:
- If there’s only 1 page, render nothing.
- Otherwise, compute
[1, 2, ..., totalPages]and render buttons for each, plus Previous/Next. - Note: For very large catalogs, rendering all page numbers may be unwieldy. Consider showing a smaller range (like
1 ... 4 5 6 ... 20) in a real-world app to improve readability and performance.
- Disabled states:
- Previous is disabled on the first page; Next on the last page — this prevents invalid navigation.
- Styling & accessibility:
- The current page button gets a highlighted style.
- The container uses
aria-label="Pagination"to help assistive tech.
When a button is clicked, onPageChange(number) updates the URL, which in turn refreshes the query and updates the UI.
Example Output:
When you visit the catalog, you might see something like this at the bottom:
If you click "Next," the app fetches the next page of books and updates the display.
URLSearchParams is a built-in Web API for building and manipulating query strings:
- Creation:
new URLSearchParams({ page: '2', q: 'clean code' }) - Encoding: Automatically escapes special characters (
q=clean+code), so you don’t have toencodeURIComponentmanually. - Editing:
.set('key', 'value')adds/replaces a parameter..delete('key')removes it..get('key')reads it; returnsnullif missing.
- Serialization:
.toString()returnskey=value&key2=value2, ready to append after a?. - Cloning from existing params:
new URLSearchParams(prev)lets you copy and modify current URL parameters cleanly (exactly what we do in the debounced search effect).
Using URLSearchParams keeps your URLs well-formed, your code concise, and your state shareable.
A key part of this setup is keeping the UI state (which page you’re on) in sync with the URL. This way, users can bookmark or share a link to a specific page, and the app will always show the correct results.
- The
useSearchParamshook from React Router reads and updates the URL’s query parameters. - When you change the page, the URL updates (for example,
?sortBy=title&order=asc&page=2). - The
useQueryhook automatically fetches new data when the page or sorting changes.
This approach makes your app more user-friendly and easier to navigate.
In this lesson, you learned how to add server-side pagination to your catalog app. You saw how to:
- Request a specific page of data from the server using query parameters.
- Update your API call and frontend to handle paginated responses.
- Use a pagination component to let users move between pages.
- Keep the UI and URL in sync for a smooth user experience.
To sum up:
- API:
getBooksbuilds/books?...withsortBy,order,page,q, andpageSizeusingURLSearchParams, then returns the typed payload. - Screen:
CatalogPagekeeps URL state in sync, debounces search input, fetches the correct page withuseQuery(fixedpageSize: 5), and computestotalPagesfrom the server response. - UI:
Paginationrenders accessible controls and calls back to update the URL, which triggers a refetch.
With these pieces, your catalog now handles search + sort + server-side pagination cleanly and efficiently — and every state is shareable via the URL. Next, you’ll get a chance to practice these concepts with hands-on exercises. This will help you reinforce what you’ve learned and make sure you can implement server-side pagination on your own. Great work — let’s keep going!
