Lesson 21 Lab — Migrate PRS Products to TanStack Query (optional)¶
Same migration as the guide, on your own PRS capstone code: Products instead of Menu Items.
Two things make this lab different from every other lab in this pass.
It runs on a branch you never merge. Your capstone as submitted stays on main. This is
how you'd actually evaluate a library at work — migrate one feature on a throwaway branch, look
at the result, then decide.
There is no reference solution. Nobody hands you a finished PRS-with-TanStack-Query to diff against, because your capstone is your code and no two are identical. You verify the way the rest of this pass verifies: the app still works in the browser, plus what the devtools panel tells you about the cache. If your files are laid out differently from the descriptions below, follow your layout — the concepts are what transfer.
Prerequisite: Lesson 21's guide, done. Your PRS API and PRS React app both running.
Part 1 — Branch, then install¶
-
In your PRS front-end repo, make sure
mainis clean and committed. -
Create the branch:
-
Install both packages:
Check: git status shows only package.json and package-lock.json changed, and you're on
react-query.
Part 2 — Provide the client¶
-
Create
src/queryClient.tsexporting a barenew QueryClient(). -
In
App.tsx, wrap the whole tree in<QueryClientProvider client={queryClient}>. It goes outside yourUserContext.Providerand the<Toaster />. -
Add
<ReactQueryDevtools initialIsOpen={false} />inside the provider.
Check: the app runs unchanged and the TanStack logo button appears in the corner. The panel lists no queries.
Part 3 — The Products list becomes one query¶
Open whichever file holds your Products fetching. If you followed the guides straight through
it's ProductsPage.tsx; if you split the grid into its own component during the capstone it's
ProductList.tsx. You're looking for the block Lesson 6 had you write — two useStates, a
loadProducts with try/catch/finally, and a useEffect with [].
-
Replace all of it with a single
useQuery. The key is["products"]; the function is your existingproductAPI.list— passed without parentheses. -
Destructure
datatoproductswith a default of[]. -
Drive the skeletons off
isPendinginstead ofloading. -
Delete the now-unused imports:
useState,useEffect, andtoast. -
Delete
removeProduct, and stop passingonRemovein the JSX.ProductCardstill declares the prop, so your editor flags the missing one — that's a type error only, and Part 4 clears it. The page renders fine; the ⋮ Delete button is the one thing that will throw until then.
Check: the Products page renders all 50 seeded products, and the devtools panel shows one
["products"] entry containing the array.
Part 4 — Delete becomes a mutation¶
ProductCard currently deletes, then calls onRemove so its parent can splice the array.
-
In
ProductCard.tsx, adduseQueryClient()and auseMutationwhosemutationFncallsproductAPI.delete. -
In its
onSuccess, invalidate["products"]and show the success toast. -
Change the ⋮ Delete handler to call
.mutate(product.id). It no longer needs to beasync. -
Remove
onRemovefrom the props interface and the function signature — the parent already stopped passing it in Part 3, so this is what clears the editor error.
Check: delete a product — the card disappears and the toast fires. In the Network tab
you see two requests: the DELETE, then a fresh GET /api/products.
Part 5 — One place for errors¶
You deleted a catch in Part 3. Stop your PRS API and reload the Products page to see what that
cost — about seven seconds of skeletons while the library retries, then an empty grid and no
explanation.
- In
queryClient.ts, pass theQueryClientaQueryCacheand aMutationCache, both with anonErrorthat toastserror.message. Leave the retry defaults alone.
Check: with the API still stopped, reload and watch the Network tab — four failed requests,
then after about seven seconds exactly one error toast saying what your
translateStatusToErrorMessage says. Then delete a product against the stopped API — that toast
is immediate, because mutations don't retry. Restart the API and reload — the list renders.
Part 6 — Verify, then leave it on the branch¶
Work through these with the API running:
- Products page loads: skeletons, then 50 cards.
- Navigate to Vendors and back to Products — no skeleton the second time.
- Change a product's price in Insomnia, click back into the browser tab — the card updates with no interaction.
- Delete a product — card goes, toast fires,
DELETEthenGETin the Network tab. - Stop the API, reload — one error toast.
Then commit, and stop:
Do not merge to main, and don't open a pull request. Your capstone stays as you submitted
it. Switch back with git switch main when you're done looking at it.
Stretch challenges¶
Optional — for when you finish early. Not needed for the capstone.
- [Reinforce] Migrate the Vendors list too. Same three moves:
useQuerywith key["vendors"], a delete mutation that invalidates it, drop theonRemoveplumbing. Notice how much shorter it is the second time. - [Reinforce] Find the right
staleTimefor Requests. A request's status changes while people are looking at the list, so the default of0is arguably correct there — while Vendors barely change at all. Set a per-querystaleTimeon the vendors query only, and prove with the Network tab that focusing the tab no longer refetches it. - [Reach] Prefetch a product on hover. Hovering a card's Edit link can warm the cache so the
edit form opens with no spinner. Not covered in the guide — you'll need
queryClient.prefetchQueryand a key that matches what the edit form will ask for. Reference: Prefetching.