From fffd36268a216044523c3f5227c3d375608c36dc Mon Sep 17 00:00:00 2001 From: nsfisis 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 --- 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 (
- + + }> + + +
-
-

Unread

- {!loading && articles.length > 0 && ( -

- {articles.length} - {hasNextPage ? "+" : ""} article - {articles.length !== 1 ? "s" : ""} to read -

- )} -
- {loading ? ( -
-

Loading unread articles...

-
- ) : error ? ( -
- Error: {error.message} -
- ) : ( - - )} +
); } + +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 ( +
+

Loading unread articles...

+
+ ); + } + + if (error) { + return ( +
+ Error: {error.message} +
+ ); + } + + return ( + <> +
+

Unread

+ {articles.length > 0 && ( +

+ {articles.length} + {hasNextPage ? "+" : ""} article + {articles.length !== 1 ? "s" : ""} to read +

+ )} +
+ fetchNextPage()} + /> + + ); +} -- cgit v1.3-1-g0d28