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/DeckDetailPage.test.tsx | 27 +++++++++++++++------------
 1 file changed, 15 insertions(+), 12 deletions(-)

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

diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx
index 903edb7..41f42fd 100644
--- a/src/client/pages/DeckDetailPage.test.tsx
+++ b/src/client/pages/DeckDetailPage.test.tsx
@@ -1,19 +1,14 @@
 /**
  * @vitest-environment jsdom
  */
+import { QueryClient } from "@tanstack/query-core";
 import { cleanup, render, screen } from "@testing-library/react";
 import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import { Route, Router } from "wouter";
 import { memoryLocation } from "wouter/memory-location";
-import {
-	authLoadingAtom,
-	type Card,
-	cardsByDeckAtomFamily,
-	type Deck,
-	deckByIdAtomFamily,
-} from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type Card, type Deck } from "../atoms";
 import { DeckDetailPage } from "./DeckDetailPage";
 
 const mockDeckGet = vi.fn();
@@ -58,6 +53,8 @@ vi.mock("../api/client", () => ({
 
 import { apiClient } from "../api/client";
 
+let testQueryClient: QueryClient;
+
 const mockDeck = {
 	id: "deck-1",
 	name: "Japanese Vocabulary",
@@ -127,17 +124,18 @@ function renderWithProviders({
 	const { hook } = memoryLocation({ path, static: true });
 	const store = createStore();
 	store.set(authLoadingAtom, false);
+	store.set(queryClientAtom, testQueryClient);
 
 	// Extract deckId from path
 	const deckIdMatch = path.match(/\/decks\/([^/]+)/);
 	const deckId = deckIdMatch?.[1] ?? "deck-1";
 
-	// Hydrate atoms if initial data provided
+	// Seed query cache if initial data provided
 	if (initialDeck !== undefined) {
-		store.set(deckByIdAtomFamily(deckId), initialDeck);
+		testQueryClient.setQueryData(["decks", deckId], initialDeck);
 	}
 	if (initialCards !== undefined) {
-		store.set(cardsByDeckAtomFamily(deckId), initialCards);
+		testQueryClient.setQueryData(["decks", deckId, "cards"], initialCards);
 	}
 
 	return render(
@@ -154,6 +152,11 @@ function renderWithProviders({
 describe("DeckDetailPage", () => {
 	beforeEach(() => {
 		vi.clearAllMocks();
+		testQueryClient = new QueryClient({
+			defaultOptions: {
+				queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+			},
+		});
 		vi.mocked(apiClient.getTokens).mockReturnValue({
 			accessToken: "access-token",
 			refreshToken: "refresh-token",
@@ -180,7 +183,7 @@ describe("DeckDetailPage", () => {
 	afterEach(() => {
 		cleanup();
 		vi.restoreAllMocks();
-		clearAtomFamilyCaches();
+		testQueryClient.clear();
 	});
 
 	it("renders back link and deck name", () => {
-- 
cgit v1.3-1-g0d28

