aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 09:26:44 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 09:26:44 +0900
commit938863425bf8ad6c17e43b3da128f92cf6d6ab63 (patch)
tree9210ea9d77c7c6fb030328cae5e8192093ebd61a
parent4ebfe21a6c89ae0518c66bc6f06b0b15bf9b7724 (diff)
downloadfeedaka-938863425bf8ad6c17e43b3da128f92cf6d6ab63.tar.gz
feedaka-938863425bf8ad6c17e43b3da128f92cf6d6ab63.tar.zst
feedaka-938863425bf8ad6c17e43b3da128f92cf6d6ab63.zip
fix(frontend): prevent layout shift in feed sidebar during loadingHEADv0.7.3main
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 <noreply@anthropic.com>
-rw-r--r--frontend/src/components/FeedSidebar.tsx78
-rw-r--r--frontend/src/pages/ReadArticles.tsx10
-rw-r--r--frontend/src/pages/UnreadArticles.tsx10
3 files changed, 56 insertions, 42 deletions
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
</button>
</li>
- {feeds.map((feed) => (
- <li key={feed.id}>
- <button
- type="button"
- onClick={() => handleSelect(feed.id)}
- className={`flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
- selectedFeedId === feed.id
- ? "bg-stone-200 font-medium text-stone-900"
- : "text-stone-600 hover:bg-stone-100"
- }`}
- >
- <span className="min-w-0 truncate">{feed.title}</span>
- {!isReadView && feed.unreadCount > 0 && (
- <span className="ml-2 shrink-0 rounded-full bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-700">
- {feed.unreadCount}
- </span>
- )}
- </button>
- </li>
- ))}
+ <ErrorBoundary>
+ <Suspense>
+ <FeedListItems
+ isReadView={isReadView}
+ selectedFeedId={selectedFeedId}
+ onSelect={handleSelect}
+ />
+ </Suspense>
+ </ErrorBoundary>
</ul>
</nav>
);
}
+
+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) => (
+ <li key={feed.id}>
+ <button
+ type="button"
+ onClick={() => onSelect(feed.id)}
+ className={`flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
+ selectedFeedId === feed.id
+ ? "bg-stone-200 font-medium text-stone-900"
+ : "text-stone-600 hover:bg-stone-100"
+ }`}
+ >
+ <span className="min-w-0 truncate">{feed.title}</span>
+ {!isReadView && feed.unreadCount > 0 && (
+ <span className="ml-2 shrink-0 rounded-full bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-700">
+ {feed.unreadCount}
+ </span>
+ )}
+ </button>
+ </li>
+ ))}
+ </>
+ );
+}
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 (
<div className="flex gap-8">
<div className="hidden w-56 shrink-0 md:block">
- <ErrorBoundary>
- <Suspense fallback={<LoadingSpinner />}>
- <FeedSidebar basePath="/read" isReadView />
- </Suspense>
- </ErrorBoundary>
+ <FeedSidebar basePath="/read" isReadView />
</div>
<div className="min-w-0 flex-1">
<ReadArticleList feedId={feedId} />
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 (
<div className="flex gap-8">
<div className="hidden w-56 shrink-0 md:block">
- <ErrorBoundary>
- <Suspense fallback={<LoadingSpinner />}>
- <FeedSidebar basePath="/unread" />
- </Suspense>
- </ErrorBoundary>
+ <FeedSidebar basePath="/unread" />
</div>
<div className="min-w-0 flex-1">
<UnreadArticleList feedId={feedId} />