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.
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.
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.
Does: Composition root: wires the data hook to the list and composer.
State: none (delegates to 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.
Does: Virtualized/windowed list. Renders only the posts near the viewport and a sentinel/loading row.
State: scroll position, virtual range
Does: Presentational post.
State: none
Does: Create a post; triggers an optimistic prepend.
State: draft text
How should we paginate the feed?
Pick when: Feeds that change while you read them (the normal case) — the cursor points at a stable item.
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.
How do new posts arrive in real time?
Pick when: Most feeds. Simplest and cheapest; the user controls when to see new content.
The data hook hides pagination behind a simple surface, and the post component is presentational with callbacks for interactions.
Hook + component contracts.
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.
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).
The rest of the deep dives:
loading="lazy" and decoding="async".Self-check: a production-ready feed covers…
0 / 8 covered
Going deeper (tap to reveal)