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/ReadArticles.tsx | 109 +++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 33 deletions(-) (limited to 'frontend/src/pages/ReadArticles.tsx') diff --git a/frontend/src/pages/ReadArticles.tsx b/frontend/src/pages/ReadArticles.tsx index 2538446..e231906 100644 --- a/frontend/src/pages/ReadArticles.tsx +++ b/frontend/src/pages/ReadArticles.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 ReadArticles() { const search = useSearch(); const params = new URLSearchParams(search); const feedId = params.get("feed"); - const { articles, hasNextPage, loading, loadingMore, loadMore, error } = - usePaginatedArticles({ isReadView: true, feedId }); + const setView = useSetAtom(articleViewAtom); + const setFeedFilter = useSetAtom(articleFeedFilterAtom); + + useEffect(() => { + setView("read"); + setFeedFilter(feedId); + }, [feedId, setView, setFeedFilter]); return (
- + + }> + + +
-
-

Read

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

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

- )} -
- {loading ? ( -
-

Loading read articles...

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

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

Read

+ {articles.length > 0 && ( +

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

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