Skip to content

Commit 55f4a9c

Browse files
Merge pull request #1009 from Nacho1499/TopicFeed-post-list
Virtualize the TopicFeed post list
2 parents 7a24a96 + b000a13 commit 55f4a9c

1 file changed

Lines changed: 58 additions & 12 deletions

File tree

src/components/social/TopicFeed.tsx

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useEffect, useRef } from 'react';
3+
import { useEffect, useRef, useState, useMemo } from 'react';
44
import Link from 'next/link';
55
import Image from 'next/image';
66
import {
@@ -62,11 +62,10 @@ function SortBar({ current, onChange, postCount }: SortBarProps) {
6262
key={value}
6363
onClick={() => onChange(value)}
6464
aria-pressed={current === value}
65-
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${
66-
current === value
65+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${current === value
6766
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
6867
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
69-
}`}
68+
}`}
7069
>
7170
{icon}
7271
{label}
@@ -82,6 +81,8 @@ interface TopicFeedProps {
8281
slug: string;
8382
}
8483

84+
const ESTIMATED_ROW_HEIGHT = 140; // Approximate height per post item in pixels
85+
8586
export default function TopicFeed({ slug }: TopicFeedProps) {
8687
const {
8788
topic,
@@ -97,7 +98,28 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
9798
followLoading,
9899
error,
99100
} = useTopicFeed(slug);
101+
100102
const sentinelRef = useRef<HTMLDivElement>(null);
103+
const containerRef = useRef<HTMLDivElement>(null);
104+
const [scrollTop, setScrollTop] = useState(0);
105+
const [containerHeight, setContainerHeight] = useState(600);
106+
107+
// Track container scroll and viewport height for virtualization
108+
useEffect(() => {
109+
const handleScroll = () => {
110+
if (containerRef.current) {
111+
setScrollTop(containerRef.current.scrollTop);
112+
}
113+
};
114+
const el = containerRef.current;
115+
if (el) {
116+
setContainerHeight(el.clientHeight || 600);
117+
el.addEventListener('scroll', handleScroll, { passive: true });
118+
}
119+
return () => {
120+
if (el) el.removeEventListener('scroll', handleScroll);
121+
};
122+
}, []);
101123

102124
// Infinite scroll via IntersectionObserver
103125
useEffect(() => {
@@ -113,6 +135,22 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
113135
return () => observer.disconnect();
114136
}, [loadMore]);
115137

138+
// Virtualization window calculations
139+
const totalPosts = posts.length;
140+
const buffer = 5;
141+
const startIndex = Math.max(0, Math.floor(scrollTop / ESTIMATED_ROW_HEIGHT) - buffer);
142+
const endIndex = Math.min(
143+
totalPosts,
144+
Math.ceil((scrollTop + containerHeight) / ESTIMATED_ROW_HEIGHT) + buffer
145+
);
146+
147+
const virtualPosts = useMemo(() => {
148+
return posts.slice(startIndex, endIndex);
149+
}, [posts, startIndex, endIndex]);
150+
151+
const topSpacerHeight = startIndex * ESTIMATED_ROW_HEIGHT;
152+
const bottomSpacerHeight = Math.max(0, (totalPosts - endIndex) * ESTIMATED_ROW_HEIGHT);
153+
116154
return (
117155
<div className="space-y-4">
118156
{/* Topic header */}
@@ -150,11 +188,10 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
150188
onClick={toggleFollow}
151189
disabled={followLoading}
152190
aria-label={topic.isFollowing ? `Unfollow #${topic.name}` : `Follow #${topic.name}`}
153-
className={`shrink-0 px-4 py-1.5 rounded-full text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-60 ${
154-
topic.isFollowing
191+
className={`shrink-0 px-4 py-1.5 rounded-full text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-60 ${topic.isFollowing
155192
? 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400'
156193
: 'bg-blue-600 text-white hover:bg-blue-700'
157-
}`}
194+
}`}
158195
>
159196
{followLoading ? '…' : topic.isFollowing ? 'Following' : 'Follow'}
160197
</button>
@@ -181,8 +218,11 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
181218
{/* Sort bar */}
182219
<SortBar current={sort} onChange={setSort} postCount={topic?.postCount} />
183220

184-
{/* Posts list */}
185-
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-800">
221+
{/* Posts list container with virtualization */}
222+
<div
223+
ref={containerRef}
224+
className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-800 max-h-[800px] overflow-y-auto relative"
225+
>
186226
{/* Initial loading skeletons */}
187227
{loading &&
188228
posts.length === 0 &&
@@ -226,8 +266,11 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
226266
</div>
227267
)}
228268

229-
{/* Post items */}
230-
{posts.map((post) => (
269+
{/* Virtualized Top Spacer */}
270+
{topSpacerHeight > 0 && <div style={{ height: `${topSpacerHeight}px` }} aria-hidden="true" />}
271+
272+
{/* Virtualized Post items */}
273+
{virtualPosts.map((post) => (
231274
<article
232275
key={post.id}
233276
className="p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
@@ -318,6 +361,9 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
318361
</article>
319362
))}
320363

364+
{/* Virtualized Bottom Spacer */}
365+
{bottomSpacerHeight > 0 && <div style={{ height: `${bottomSpacerHeight}px` }} aria-hidden="true" />}
366+
321367
{/* Infinite scroll sentinel */}
322368
{hasMore && <div ref={sentinelRef} className="h-4" aria-hidden="true" />}
323369

@@ -331,4 +377,4 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
331377
</div>
332378
</div>
333379
);
334-
}
380+
}

0 commit comments

Comments
 (0)