Skip to content

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

  1. In your PRS front-end repo, make sure main is clean and committed.

  2. Create the branch:

    git switch -c react-query
    
  3. Install both packages:

    npm install @tanstack/react-query
    npm install -D @tanstack/react-query-devtools
    

Check: git status shows only package.json and package-lock.json changed, and you're on react-query.


Part 2 — Provide the client

  1. Create src/queryClient.ts exporting a bare new QueryClient().

  2. In App.tsx, wrap the whole tree in <QueryClientProvider client={queryClient}>. It goes outside your UserContext.Provider and the <Toaster />.

  3. 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 [].

  1. Replace all of it with a single useQuery. The key is ["products"]; the function is your existing productAPI.list — passed without parentheses.

  2. Destructure data to products with a default of [].

  3. Drive the skeletons off isPending instead of loading.

  4. Delete the now-unused imports: useState, useEffect, and toast.

  5. Delete removeProduct, and stop passing onRemove in the JSX. ProductCard still 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.

  1. In ProductCard.tsx, add useQueryClient() and a useMutation whose mutationFn calls productAPI.delete.

  2. In its onSuccess, invalidate ["products"] and show the success toast.

  3. Change the ⋮ Delete handler to call .mutate(product.id). It no longer needs to be async.

  4. Remove onRemove from 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.

  1. In queryClient.ts, pass the QueryClient a QueryCache and a MutationCache, both with an onError that toasts error.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, DELETE then GET in the Network tab.
  • Stop the API, reload — one error toast.

Then commit, and stop:

git add -A && git commit -m "Try TanStack Query on the Products list"

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: useQuery with key ["vendors"], a delete mutation that invalidates it, drop the onRemove plumbing. Notice how much shorter it is the second time.
  • [Reinforce] Find the right staleTime for Requests. A request's status changes while people are looking at the list, so the default of 0 is arguably correct there — while Vendors barely change at all. Set a per-query staleTime on 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.prefetchQuery and a key that matches what the edit form will ask for. Reference: Prefetching.