Design a news/social feed with infinite scroll. Apply RADIO; call out pagination and performance.
Pagination strategy, list performance, and loading/error UX at scale.
Requirements: endless list, fast scroll, resilient to slow network, accessible. Architecture: a Feed container fetches pages and renders a virtualized list of Post components; an IntersectionObserver sentinel near the bottom triggers the next page. Data model: items array + cursor/nextPageToken + isLoading/hasMore/error. Interface: cursor-based pagination (GET /feed?cursor=…&limit=20) — prefer cursors over offset (stable under inserts, no skipped/duplicated items). Optimizations: virtualize the list so the DOM stays small; cache pages; show skeletons while loading; handle error with a retry; debounce/guard so you don't fire duplicate page requests; preserve scroll position on back-navigation. Why IntersectionObserver over a scroll listener: it fires off the main thread and avoids the jank of high-frequency scroll + getBoundingClientRect measurement — place a sentinel div after the last item and load when it intersects. Cursor vs offset: offset pagination (?page=3) skips or duplicates rows when items are inserted or deleted between fetches; an opaque cursor/nextPageToken points at a stable position, so the feed stays consistent under live writes. Virtualization matters because a feed can grow to thousands of nodes — windowing (react-window / react-virtualized) keeps only the visible rows in the DOM so scrolling stays smooth and memory stays flat. Guard against duplicate in-flight requests with loading/hasMore flags, prefer skeletons over spinners to reduce layout shift, and always give errors a retry affordance.
Feeds, search results, chat history, any long server-paginated list.
function Feed({ fetchPage }) {
const [items, setItems] = React.useState([]);
const [cursor, setCursor] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [hasMore, setHasMore] = React.useState(true);
const sentinel = React.useRef(null);
const loadMore = React.useCallback(async () => {
if (loading || !hasMore) return; // guard duplicate requests
setLoading(true);
const { data, nextCursor } = await fetchPage(cursor);
setItems(prev => [...prev, ...data]);
setCursor(nextCursor);
setHasMore(Boolean(nextCursor));
setLoading(false);
}, [cursor, loading, hasMore, fetchPage]);
React.useEffect(() => {
const io = new IntersectionObserver(
([e]) => e.isIntersecting && loadMore()
);
if (sentinel.current) io.observe(sentinel.current);
return () => io.disconnect();
}, [loadMore]);
return (<>{items.map(p => <Post key={p.id} {...p} />)}
<div ref={sentinel}>{loading ? 'Loading…' : ''}</div></>);
}