From a07dada0c9ba80976692ee14e256da0a2d6b0112 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 1 Feb 2026 10:16:35 +0900
Subject: refactor(atoms): migrate to jotai-tanstack-query from custom
 reloadable atoms

Replace custom createReloadableAtom/createReloadableAtomFamily with
atomWithSuspenseQuery from jotai-tanstack-query, leveraging TanStack
Query's built-in caching, invalidation, and Suspense support.

- Add @tanstack/query-core and jotai-tanstack-query dependencies
- Create shared QueryClient instance (src/client/queryClient.ts)
- Migrate all data atoms (decks, cards, study, noteTypes) to atomWithSuspenseQuery
- Update page components to use .data destructuring and queryClient.invalidateQueries()
- Update all test files to use QueryClient for data hydration
- Remove src/client/atoms/utils.ts (no longer needed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/pages/HomePage.test.tsx | 32 ++++++++++++++++++++++++++------
 1 file changed, 26 insertions(+), 6 deletions(-)

(limited to 'src/client/pages/HomePage.test.tsx')

diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx
index 8946fcf..8b17506 100644
--- a/src/client/pages/HomePage.test.tsx
+++ b/src/client/pages/HomePage.test.tsx
@@ -2,15 +2,16 @@
  * @vitest-environment jsdom
  */
 import "fake-indexeddb/auto";
+import { QueryClient } from "@tanstack/query-core";
 import { cleanup, render, screen, waitFor } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import { Router } from "wouter";
 import { memoryLocation } from "wouter/memory-location";
 import { apiClient } from "../api/client";
-import { authLoadingAtom, type Deck, decksAtom } from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type Deck } from "../atoms";
 import { HomePage } from "./HomePage";
 
 const mockDeckPut = vi.fn();
@@ -51,6 +52,14 @@ vi.mock("../api/client", () => ({
 	},
 }));
 
+// Mock queryClient module so pages use our test queryClient
+let testQueryClient: QueryClient;
+vi.mock("../queryClient", () => ({
+	get queryClient() {
+		return testQueryClient;
+	},
+}));
+
 // Mock fetch globally for Edit/Delete modals
 const mockFetch = vi.fn();
 global.fetch = mockFetch;
@@ -109,10 +118,11 @@ function renderWithProviders({
 	const { hook } = memoryLocation({ path });
 	const store = createStore();
 	store.set(authLoadingAtom, false);
+	store.set(queryClientAtom, testQueryClient);
 
-	// If initialDecks provided, hydrate the atom to skip Suspense
+	// If initialDecks provided, seed query cache to skip Suspense
 	if (initialDecks !== undefined) {
-		store.set(decksAtom, initialDecks);
+		testQueryClient.setQueryData(["decks"], initialDecks);
 	}
 
 	return render(
@@ -127,7 +137,11 @@ function renderWithProviders({
 describe("HomePage", () => {
 	beforeEach(() => {
 		vi.clearAllMocks();
-		clearAtomFamilyCaches();
+		testQueryClient = new QueryClient({
+			defaultOptions: {
+				queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+			},
+		});
 		vi.mocked(apiClient.getTokens).mockReturnValue({
 			accessToken: "access-token",
 			refreshToken: "refresh-token",
@@ -152,7 +166,7 @@ describe("HomePage", () => {
 	afterEach(() => {
 		cleanup();
 		vi.restoreAllMocks();
-		clearAtomFamilyCaches();
+		testQueryClient.clear();
 	});
 
 	it("renders page title and logout button", () => {
@@ -260,6 +274,12 @@ describe("HomePage", () => {
 	});
 
 	it("passes auth header when fetching decks", async () => {
+		testQueryClient = new QueryClient({
+			defaultOptions: {
+				queries: { staleTime: 0, retry: false },
+			},
+		});
+
 		vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue(
 			mockResponse({
 				ok: true,
-- 
cgit v1.3-1-g0d28

