Skip to content

Lesson 22 Lab — Mutations and Pagination on PRS Products (optional)

Same work as the guide, on your own PRS code: the Product form's reads and writes, then paging the Products list. Your PRS seed already has 50 products, so there's nothing to top up — that's five pages of twelve.

Still on the react-query branch you never merge, and still no reference solution — you verify in the browser and in the devtools panel.

Prerequisite: Lesson 22's guide, and Lesson 21's lab finished on your react-query branch.


Part 1 — Back on the branch

  1. git switch react-query in your PRS front-end repo, and confirm Part 6 of the Lesson 21 lab is committed there.

  2. Start your PRS API and your React app.

Check: the Products page loads through useQuery and the devtools panel shows ["products"].


Part 2 — The form's two reads

ProductForm needs vendors (always, for the dropdown) and the product (only when the route has an :id). Right now both happen inside react-hook-form's async defaultValues.

  1. Replace the vendors state and its loader with a useQuery on ["vendors"].

  2. Add a second useQuery on ["products", Number(id)] with enabled: !!id.

  3. Destructure isLoading from both — not isPending. A disabled query stays pending forever, so isPending would leave the Create page spinning.

  4. Replace useForm's defaultValues with values: product ?? emptyProduct.

  5. Just above the return, after every hook, add the guard that renders a spinner-border while either query is loading.

Check: open Edit on a product — Number, Name, Price, Unit and Vendor are all filled in. The Vendor dropdown is the one to watch; skip the guard and it comes up blank.


Part 3 — Save becomes a mutation

  1. Replace the save handler's try/catch with a useMutation. Its mutationFn PUTs when product.id exists and POSTs when it doesn't.

  2. In onSuccess, invalidate ["products"], toast, and navigate to /products.

  3. Reduce save to one line that calls .mutate(product).

  4. Disable the Save button on the mutation's isPending, and swap its label while saving.

Check: edit a product's price and save — the button reads "Saving…" and is disabled, then the list shows the new price. The Network tab shows PUT, then a fresh GET /api/products.


Part 4 — A paged endpoint on Prs.Api

Back to C#. This is the same pair of pieces the guide added to MenuItemsController, and you write it from that model — there is no PRS version handed to you.

  1. Add Models/PagedResult.cs — a generic class with Items, Page, PageSize, and TotalCount.

  2. Add a GetPaged action to ProductsController on the route paged, taking page and pageSize from the query string with sensible defaults.

  3. Include the vendor, CountAsync() the whole set before slicing, then OrderBy the id, Skip((page - 1) * pageSize), Take(pageSize).

  4. Leave GetAll and GetById exactly as they are.

Check: with the API running, paste http://localhost:5555/api/products/paged?page=1&pageSize=12 into the browser — 12 items, each with a nested vendor, and "totalCount": 50. Then check /api/products still returns the plain array of all 50, and /api/products/1 still returns one product. Use your own API's port if it differs.


Part 5 — Page in the query key

  1. Add src/utility/IPagedResult.ts mirroring the C# shape.

  2. Add listPaged(page, pageSize) to ProductAPI.ts, beside the existing list — which stays.

  3. In the same file you changed in Lesson 21's lab: useState for page, a query key of ["products", { page }], a queryFn arrow that calls listPaged, and placeholderData: keepPreviousData.

  4. Read the cards from data?.items ?? [] and compute totalPages from data?.totalCount.

  5. Add a Previous / "Page X of Y" / Next bar below the grid. Previous disables on page 1; Next disables on isPlaceholderData || page === totalPages. If your component's return has no element to hold both the grid and the bar, wrap them in a fragment (<>…</>).

Check: 12 cards and "Page 1 of 5". Throttle to Slow 3G (DevTools → Network), then click Next — the grid never goes blank and Next stays disabled until the new page lands. The devtools panel grows one entry per page you visit, and clicking Previous is instant.


Part 6 — Verify, then stop

  • Products page: 12 cards, "Page 1 of 5", Previous disabled.
  • Paging forward and back never blanks the grid; on Slow 3G, Next is disabled mid-load.
  • Delete a product on page 2 — the toast fires and page 2 refetches, not page 1.
  • Edit and save a product — "Saving…", then the updated value in the list.
  • Create a product — the form opens empty with a filled Vendor dropdown.
  • Stop the API and reload — exactly one error toast.

Commit, and leave it here:

git add -A && git commit -m "Paginate Products and move the form to mutations"

Do not merge to main. git switch main when you're done. Your capstone is unchanged, and you now have a branch you can diff against it to see what a server-state library actually costs and buys.


Stretch challenges

Optional — for when you finish early. Not needed for the capstone.

  • [Reinforce] Put the Requests status filter in a query key. Your Requests list already refetches on ?status= — replace whatever effect drives that with a key of ["requests", { status }]. Then switch back and forth between two statuses and watch the devtools panel serve the second one from cache.
  • [Reinforce] Page size as state. Add a 12 / 24 / 48 dropdown and put pageSize in the key alongside page. Decide what should happen to the current page number when the size changes, and make it happen.
  • [Reach] Keep the page number in the URL. ?page=3 should survive a refresh and a back button, which means the page belongs in the router's search params rather than useState. Not covered in the guide — you'll need useSearchParams. Reference: useSearchParams.