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/components/FeedSidebar.tsx | 26 +++-----------------------
 1 file changed, 3 insertions(+), 23 deletions(-)

(limited to 'frontend/src/components/FeedSidebar.tsx')

diff --git a/frontend/src/components/FeedSidebar.tsx b/frontend/src/components/FeedSidebar.tsx
index 4f50566..6c385c5 100644
--- a/frontend/src/components/FeedSidebar.tsx
+++ b/frontend/src/components/FeedSidebar.tsx
@@ -1,9 +1,6 @@
-import { useCallback, useEffect, useState } from "react";
+import { useAtomValue } from "jotai";
 import { useLocation, useSearch } from "wouter";
-import type { components } from "../api/generated";
-import { api } from "../services/api-client";
-
-type Feed = components["schemas"]["Feed"];
+import { feedsAtom } from "../atoms";
 
 interface Props {
 	basePath: string;
@@ -15,21 +12,7 @@ export function FeedSidebar({ basePath }: Props) {
 	const params = new URLSearchParams(search);
 	const selectedFeedId = params.get("feed");
 
-	const [feeds, setFeeds] = useState<Feed[]>([]);
-	const [fetching, setFetching] = useState(true);
-
-	const fetchFeeds = useCallback(async () => {
-		setFetching(true);
-		const { data } = await api.GET("/api/feeds");
-		if (data) {
-			setFeeds(data);
-		}
-		setFetching(false);
-	}, []);
-
-	useEffect(() => {
-		fetchFeeds();
-	}, [fetchFeeds]);
+	const { data: feeds } = useAtomValue(feedsAtom);
 
 	const handleSelect = (feedId: string | null) => {
 		if (feedId) {
@@ -58,9 +41,6 @@ export function FeedSidebar({ basePath }: Props) {
 						All feeds
 					</button>
 				</li>
-				{fetching && (
-					<li className="px-3 py-1.5 text-xs text-stone-400">Loading...</li>
-				)}
 				{feeds.map((feed) => (
 					<li key={feed.id}>
 						<button
-- 
cgit v1.3-1-g0d28

