From 938863425bf8ad6c17e43b3da128f92cf6d6ab63 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 21 Feb 2026 09:26:44 +0900 Subject: fix(frontend): prevent layout shift in feed sidebar during loading Show "All feeds" button immediately and load individual feeds asynchronously via internal Suspense boundary, avoiding the spinner-to-content shift that occurred with the outer Suspense wrapper. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/FeedSidebar.tsx | 78 ++++++++++++++++++++++----------- frontend/src/pages/ReadArticles.tsx | 10 +---- frontend/src/pages/UnreadArticles.tsx | 10 +---- 3 files changed, 56 insertions(+), 42 deletions(-) (limited to 'frontend') diff --git a/frontend/src/components/FeedSidebar.tsx b/frontend/src/components/FeedSidebar.tsx index 209be23..3a367f5 100644 --- a/frontend/src/components/FeedSidebar.tsx +++ b/frontend/src/components/FeedSidebar.tsx @@ -1,6 +1,8 @@ import { useAtomValue } from "jotai"; +import { Suspense } from "react"; import { useLocation, useSearch } from "wouter"; import { feedsAtom } from "../atoms"; +import { ErrorBoundary } from "./ErrorBoundary"; interface Props { basePath: string; @@ -13,12 +15,6 @@ export function FeedSidebar({ basePath, isReadView = false }: Props) { const params = new URLSearchParams(search); const selectedFeedId = params.get("feed"); - const { data: allFeeds } = useAtomValue(feedsAtom); - - const feeds = isReadView - ? allFeeds - : allFeeds.filter((feed) => feed.unreadCount > 0); - const handleSelect = (feedId: string | null) => { if (feedId) { setLocation(`${basePath}?feed=${feedId}`); @@ -46,27 +42,57 @@ export function FeedSidebar({ basePath, isReadView = false }: Props) { All feeds - {feeds.map((feed) => ( -
  • - -
  • - ))} + + + + + ); } + +function FeedListItems({ + isReadView, + selectedFeedId, + onSelect, +}: { + isReadView: boolean; + selectedFeedId: string | null; + onSelect: (feedId: string | null) => void; +}) { + const { data: allFeeds } = useAtomValue(feedsAtom); + + const feeds = isReadView + ? allFeeds + : allFeeds.filter((feed) => feed.unreadCount > 0); + + return ( + <> + {feeds.map((feed) => ( +
  • + +
  • + ))} + + ); +} diff --git a/frontend/src/pages/ReadArticles.tsx b/frontend/src/pages/ReadArticles.tsx index 8fcf9df..4d70558 100644 --- a/frontend/src/pages/ReadArticles.tsx +++ b/frontend/src/pages/ReadArticles.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom } from "jotai"; -import { Suspense, useEffect } from "react"; +import { useEffect } from "react"; import { useSearch } from "wouter"; import { articleFeedFilterAtom, @@ -7,9 +7,7 @@ import { 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(); @@ -27,11 +25,7 @@ export function ReadArticles() { return (
    - - }> - - - +
    diff --git a/frontend/src/pages/UnreadArticles.tsx b/frontend/src/pages/UnreadArticles.tsx index 32d1190..e4754a3 100644 --- a/frontend/src/pages/UnreadArticles.tsx +++ b/frontend/src/pages/UnreadArticles.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom } from "jotai"; -import { Suspense, useEffect } from "react"; +import { useEffect } from "react"; import { useSearch } from "wouter"; import { articleFeedFilterAtom, @@ -7,9 +7,7 @@ import { 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(); @@ -27,11 +25,7 @@ export function UnreadArticles() { return (
    - - }> - - - +
    -- cgit v1.3-1-g0d28