From fffd36268a216044523c3f5227c3d375608c36dc Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sat, 14 Feb 2026 12:17:23 +0900
Subject: refactor(frontend): migrate state management to jotai and
 jotai-tanstack-query

Replace React Context + manual useEffect data fetching with jotai atoms
for state management and jotai-tanstack-query for server state caching.

- Add jotai, jotai-tanstack-query, @tanstack/query-core dependencies
- Create atoms for auth (primitive + action), feeds (suspense query),
  and articles (infinite query with cursor-based pagination)
- Wire up Provider, HydrateQueryClient, and StoreInitializer in main.tsx
- Migrate all components from useAuth() context to jotai atoms
- Replace manual fetch logic in FeedSidebar/FeedList with feedsAtom
- Replace usePaginatedArticles hook with articlesInfiniteAtom
- Add queryClient.invalidateQueries() after mutations for automatic
  cache refresh
- Add ErrorBoundary and LoadingSpinner components for Suspense support
- Remove callback prop chains (onFeedAdded, onFeedChanged, etc.)
  in favor of query invalidation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 frontend/src/pages/UnreadArticles.tsx | 109 ++++++++++++++++++++++++----------
 1 file changed, 76 insertions(+), 33 deletions(-)

(limited to 'frontend/src/pages/UnreadArticles.tsx')

diff --git a/frontend/src/pages/UnreadArticles.tsx b/frontend/src/pages/UnreadArticles.tsx
index eade6fc..291f0ee 100644
--- a/frontend/src/pages/UnreadArticles.tsx
+++ b/frontend/src/pages/UnreadArticles.tsx
@@ -1,48 +1,91 @@
+import { useAtomValue, useSetAtom } from "jotai";
+import { Suspense, useEffect } from "react";
 import { useSearch } from "wouter";
-import { ArticleList, FeedSidebar } from "../components";
-import { usePaginatedArticles } from "../hooks/usePaginatedArticles";
+import {
+	articleFeedFilterAtom,
+	articlesInfiniteAtom,
+	articleViewAtom,
+} from "../atoms";
+import { ArticleList } from "../components/ArticleList";
+import { ErrorBoundary } from "../components/ErrorBoundary";
+import { FeedSidebar } from "../components/FeedSidebar";
+import { LoadingSpinner } from "../components/LoadingSpinner";
 
 export function UnreadArticles() {
 	const search = useSearch();
 	const params = new URLSearchParams(search);
 	const feedId = params.get("feed");
 
-	const { articles, hasNextPage, loading, loadingMore, loadMore, error } =
-		usePaginatedArticles({ isReadView: false, feedId });
+	const setView = useSetAtom(articleViewAtom);
+	const setFeedFilter = useSetAtom(articleFeedFilterAtom);
+
+	useEffect(() => {
+		setView("unread");
+		setFeedFilter(feedId);
+	}, [feedId, setView, setFeedFilter]);
 
 	return (
 		<div className="flex gap-8">
-			<FeedSidebar basePath="/unread" />
+			<ErrorBoundary>
+				<Suspense fallback={<LoadingSpinner />}>
+					<FeedSidebar basePath="/unread" />
+				</Suspense>
+			</ErrorBoundary>
 			<div className="min-w-0 flex-1">
-				<div className="mb-6">
-					<h1 className="text-xl font-semibold text-stone-900">Unread</h1>
-					{!loading && articles.length > 0 && (
-						<p className="mt-1 text-sm text-stone-400">
-							{articles.length}
-							{hasNextPage ? "+" : ""} article
-							{articles.length !== 1 ? "s" : ""} to read
-						</p>
-					)}
-				</div>
-				{loading ? (
-					<div className="py-8 text-center">
-						<p className="text-sm text-stone-400">Loading unread articles...</p>
-					</div>
-				) : error ? (
-					<div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
-						Error: {error.message}
-					</div>
-				) : (
-					<ArticleList
-						articles={articles}
-						isReadView={false}
-						isSingleFeed={!!feedId}
-						hasNextPage={hasNextPage}
-						loadingMore={loadingMore}
-						onLoadMore={loadMore}
-					/>
-				)}
+				<UnreadArticleList feedId={feedId} />
 			</div>
 		</div>
 	);
 }
+
+function UnreadArticleList({ feedId }: { feedId: string | null }) {
+	const {
+		data,
+		isLoading,
+		isFetchingNextPage,
+		hasNextPage,
+		fetchNextPage,
+		error,
+	} = useAtomValue(articlesInfiniteAtom);
+
+	const articles = data?.pages.flatMap((page) => page.articles) ?? [];
+
+	if (isLoading) {
+		return (
+			<div className="py-8 text-center">
+				<p className="text-sm text-stone-400">Loading unread articles...</p>
+			</div>
+		);
+	}
+
+	if (error) {
+		return (
+			<div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
+				Error: {error.message}
+			</div>
+		);
+	}
+
+	return (
+		<>
+			<div className="mb-6">
+				<h1 className="text-xl font-semibold text-stone-900">Unread</h1>
+				{articles.length > 0 && (
+					<p className="mt-1 text-sm text-stone-400">
+						{articles.length}
+						{hasNextPage ? "+" : ""} article
+						{articles.length !== 1 ? "s" : ""} to read
+					</p>
+				)}
+			</div>
+			<ArticleList
+				articles={articles}
+				isReadView={false}
+				isSingleFeed={!!feedId}
+				hasNextPage={hasNextPage}
+				loadingMore={isFetchingNextPage}
+				onLoadMore={() => fetchNextPage()}
+			/>
+		</>
+	);
+}
-- 
cgit v1.3-1-g0d28

