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-querybranch.
Part 1 — Back on the branch¶
-
git switch react-queryin your PRS front-end repo, and confirm Part 6 of the Lesson 21 lab is committed there. -
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.
-
Replace the vendors state and its loader with a
useQueryon["vendors"]. -
Add a second
useQueryon["products", Number(id)]withenabled: !!id. -
Destructure
isLoadingfrom both — notisPending. A disabled query stays pending forever, soisPendingwould leave the Create page spinning. -
Replace
useForm'sdefaultValueswithvalues: product ?? emptyProduct. -
Just above the
return, after every hook, add the guard that renders aspinner-borderwhile 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¶
-
Replace the
savehandler'stry/catchwith auseMutation. ItsmutationFnPUTs whenproduct.idexists and POSTs when it doesn't. -
In
onSuccess, invalidate["products"], toast, and navigate to/products. -
Reduce
saveto one line that calls.mutate(product). -
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.
-
Add
Models/PagedResult.cs— a generic class withItems,Page,PageSize, andTotalCount. -
Add a
GetPagedaction toProductsControlleron the routepaged, takingpageandpageSizefrom the query string with sensible defaults. -
Includethe vendor,CountAsync()the whole set before slicing, thenOrderBythe id,Skip((page - 1) * pageSize),Take(pageSize). -
Leave
GetAllandGetByIdexactly 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¶
-
Add
src/utility/IPagedResult.tsmirroring the C# shape. -
Add
listPaged(page, pageSize)toProductAPI.ts, beside the existinglist— which stays. -
In the same file you changed in Lesson 21's lab:
useStateforpage, a query key of["products", { page }], aqueryFnarrow that callslistPaged, andplaceholderData: keepPreviousData. -
Read the cards from
data?.items ?? []and computetotalPagesfromdata?.totalCount. -
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'sreturnhas 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:
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
pageSizein the key alongsidepage. 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=3should survive a refresh and a back button, which means the page belongs in the router's search params rather thanuseState. Not covered in the guide — you'll needuseSearchParams. Reference:useSearchParams.