# Infinite Scroll & Feeds
> Cursor-paginated feed with a bounded DOM and accessible fallback.
Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/infinite-scroll-feed
Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end.
## Summary
A production feed uses keyset/cursor pagination — not offset — so skips and duplicates don't occur as data shifts. An `IntersectionObserver` sentinel triggers the next page load automatically. TanStack Virtual keeps the rendered DOM bounded regardless of list length. A 'load more' button preserves keyboard and screen-reader access.
## Definition
An infinite-scroll feed auto-fetches the next page as the user nears the bottom, creating the appearance of a continuous list.
The two foundational choices drive every other decision:
- **Cursor vs offset pagination.** Offset pagination (`LIMIT n OFFSET k`) produces skips and duplicates when rows are inserted while the user scrolls. A keyset cursor (e.g. `after: "2xZ9q"`) encodes the last item's sort key; inserts don't shift it.
- **Observer vs scroll event.** A scroll-event listener fires hundreds of times per scroll and lives on the main thread. An `IntersectionObserver` sentinel fires once, off the critical path, when an invisible marker enters the viewport.
## Diagram
```mermaid
sequenceDiagram
participant V as Viewport
participant S as Sentinel
participant IO as IntersectionObserver
participant H as useInfiniteQuery
participant API as API
V->>S: sentinel enters viewport
S->>IO: threshold crossed
IO->>H: callback fires → fetchNextPage()
H->>API: GET /feed?after=cursor
API-->>H: {items, nextCursor}
H-->>V: appends page to list
```
Sentinel visibility triggers a single observer callback; no scroll listener needed.
## IntersectionObserver sentinel hook
The hook attaches an observer to a ref, calls `onIntersect` once when visible, and disconnects on unmount. Wire the ref to an empty `
` after the list.
**useIntersectionSentinel — observer hook**
```tsx
import { useEffect, useRef } from "react";
interface Options {
onIntersect: () => void;
enabled?: boolean;
rootMargin?: string;
}
/** Attach to a sentinel
{/* Sentinel — invisible; triggers next fetch */}
{/* Accessible fallback for keyboard / SR users */}
{hasNextPage && (
)}
);
}
```
`rootMargin: '200px'` pre-fetches before the sentinel is fully visible. The button fallback ensures keyboard and screen-reader users can advance the feed.
## Watch out: Virtualize once the list exceeds ~200 rows
Without windowing, every page appended keeps its DOM nodes alive. At 1,000+ items this causes layout thrash and high memory. Add `@tanstack/react-virtual` — measure each row, render only the visible window (~30 nodes), and set a spacer div equal to the total estimated height. Scroll restoration: save `scrollTop` in session storage on `popstate` and restore after the initial page hydrates.
## Key terms
- **keyset pagination**: Paginate by encoding the last-seen row's sort key as a cursor; immune to inserts shifting offsets.
- **sentinel element**: An empty DOM node at the list bottom; `IntersectionObserver` fires when it enters the viewport.
- **list virtualization**: Render only visible rows plus a small overscan; TanStack Virtual manages row measurement and offsets.
- **scroll restoration**: Saving and re-applying scroll position so back-navigation returns users to their place in the feed.
- **IntersectionObserver**: Browser API that fires a callback when a target element crosses a viewport threshold; no scroll listener needed.
## Related topics
- [Server State & Data Fetching](https://fearchitect.com/topics/server-state-data-fetching.md): Async, shared, remote-owned data that requires a dedicated cache layer.
- [Render Performance](https://fearchitect.com/topics/render-performance-patterns.md): Skip renders, defer slow work, and virtualize long lists.
- [Data Table / Data Grid](https://fearchitect.com/topics/data-table.md): Headless table logic separate from rendering, with server-side ops for large datasets.
- [Accessibility (a11y)](https://fearchitect.com/topics/accessibility.md): WCAG 2.2 AA: semantic HTML, keyboard nav, and ARIA done right.
- [Modal & Dialog System](https://fearchitect.com/topics/modal-dialog-system.md): Native