News Feed (Infinite Scroll)

mediumApplication

A news feed is the classic front-end application design: an endless list of rich content that must stay smooth on a phone, update without losing the user's place, and feel instant. We'll use the RADIO framework — Requirements, Architecture, Data model, Interface, Optimizations.

R

Requirements

Scope the feed before designing it — a chronological text feed and a ranked media feed lead to very different decisions.

Before designing, what should you nail down?

Functional vs non-functional

Functional: browse an endless feed, like/comment, create a post. Non-functional: 60fps scrolling on mobile, fast first paint, no layout shift as media loads, never lose scroll position, resilient to flaky networks.

A

Architecture

Separate data from presentation: a hook owns fetching and pagination, a list component owns windowing, and dumb post components render content. This keeps the expensive concerns (network, rendering) isolated and testable.

Click a node to see what it owns.

<Feed>

Does: Composition root: wires the data hook to the list and composer.

State: none (delegates to useFeed)

useFeed()

Does: Fetches pages with cursor pagination, exposes posts + fetchNextPage + status, handles optimistic create.

State: pages, nextCursor, status, isFetchingNextPage

Often React Query's useInfiniteQuery — gives caching, dedup, and retries for free.

<FeedList>

Does: Virtualized/windowed list. Renders only the posts near the viewport and a sentinel/loading row.

State: scroll position, virtual range

<PostCard>

Does: Presentational post.

State: none

<PostHeader>
<PostMedia>
<PostActions>
<NewPostComposer>

Does: Create a post; triggers an optimistic prepend.

State: draft text

How should we paginate the feed?

Common default

Pick when: Feeds that change while you read them (the normal case) — the cursor points at a stable item.

PROS
  • Stable when items are inserted/removed — no skipped or duplicated posts
  • Efficient at the database layer (no large OFFSET scans)
CONS
  • Can't jump to an arbitrary page
  • Cursor must be opaque + stable
D

Data Model

Model the feed as pages (each with a nextCursor) flattened into a single list for rendering. Track optimistic posts with a temporary id until the server returns the real one.

Core types.

Loading editor…

How do new posts arrive in real time?

Common default

Pick when: Most feeds. Simplest and cheapest; the user controls when to see new content.

PROS
  • No persistent connection
  • No surprise content shifts while reading
CONS
  • Not truly live
I

Interface

The data hook hides pagination behind a simple surface, and the post component is presentational with callbacks for interactions.

Hook + component contracts.

Loading editor…
O

Optimizations & Deep Dives

The four problems that make or break a feed: windowing (smooth scroll at scale), cursor pagination (correctness under change), optimistic updates (instant feel), and media handling (no layout shift).

The signature insight: render only what's visible

A feed of 5,000 posts is 5,000 DOM subtrees — layout and paint costs scale with DOM size, so scrolling janks and memory balloons. Windowing (a.k.a. virtualization) renders only the ~10-15 posts near the viewport plus a small overscan, keeping the DOM tiny and constant regardless of feed length. That's what holds 60fps on a phone.

Infinite scroll + optimistic posting (editable)

Cursor pagination via an IntersectionObserver sentinel, plus an optimistic 'New post' that appears instantly then confirms. Scroll to load more.

Loading editor…

For very long feeds, add windowing on top of the infinite loading above. The trigger and the windowing cooperate — the last virtual row is the loading row:

Windowing with @tanstack/react-virtual (illustrative).

Loading editor…

The rest of the deep dives:

  • Optimistic create/like: update the UI immediately with a temp id, reconcile (or roll back) when the server responds. The demo shows the create path.
  • Scroll restoration: when navigating into a post and back, restore the exact scroll offset (cache it per route) so users don't lose their place.
  • No layout shift (CLS): give media a known aspect ratio so images don't push content down as they load; use loading="lazy" and decoding="async".
  • Errors & empties: per-page retry, a clear empty state, and a 'failed to load more' row that's retryable.

Self-check: a production-ready feed covers…

0 / 8 covered

Going deeper (tap to reveal)