UseFeedOptions
Loads a feed’s first page and keeps it fresh, with optimistic loadNext/addActivity/
refresh helpers.
Returns { activities, loadNext, hasNext, isLoading, error, addActivity, refresh, newCount, showNew, checkNew }, plus the infinite-scroll set: canLoadMore, retry,
isLoadingInitial, isLoadingMore.
INFINITE SCROLL. Driving loadNext from an IntersectionObserver is not the same
problem as driving it from a button — the observer fires on intersect and again on
every reflow, so what a button reaches once, a sentinel reaches constantly:
- Bind the sentinel to
canLoadMore, nothasNext. It also folds in “a page is already in flight” and “the last page failed and nobody has acknowledged it”. - Gate a full-page spinner on
isLoadingInitial, never onisLoading. The shared flag is true duringloadNexttoo, so a list gated on it unmounts its own sentinel mid-fetch and scrolling stops for good. loadNextis internally guarded anyway: a call while a page is in flight, or whileerroris set, is a no-op rather than a duplicate request on the same cursor. Pages merge deduped by id, so an overlapping page cannot produce duplicate keys.retry()is the only way past the error guard. Wire it to a button.
opts.initialData
Server-prefetched page for SSR/SSG hydration, e.g.
{ initialData: await server.feed(group, id).get() } (the shape is @dropinnodex/client’s
Page<Activity<TCustom>>, so a server-side feed read passes through verbatim).
When present, the hook renders that data on the very first render with isLoading: false
— no loading flash — while the mount effect still fires in the background to
revalidate against the live feed. A warm provider cache (a prior fetch this session)
always wins over initialData, since it is fresher.
opts.pollInterval (deprecated — prefer live) is milliseconds between automatic background checkNew() calls
(the Twitter/IG “N new posts ↑” pattern). Omit or pass 0/negative to disable — nothing
polls unless this is set. New activities are never auto-prepended: they land in a buffer
(newCount) until the app calls showNew(), so an open feed never jumps under the reader.
The timer is cleared on unmount and reset whenever pollInterval changes.
checkNew() fetches page 1 and buffers (does not prepend) any activity newer than what’s
currently shown, deduped by id against both the shown list and anything already buffered —
so a round-tripped addActivity (write, then seen again on the next poll) is never counted
as “new”. newCount is the buffered count; call showNew() to prepend the buffer into
activities and clear it. Both are headless — this hook does not render the “N new” pill,
the app does. The one carve-out from “buffers, never prepends”: calling checkNew() on a
feed that is currently EMPTY loads the result straight into activities (there is nothing
to jump over), so newCount stays 0 in that case. Caveat: page 1 is fetched at a fixed
size of 20, so newCount saturates at 20 (it cannot tell 20 new activities from 200+ new
activities) — apps displaying the count should render it as “20+” (or similar) whenever
newCount reaches that page size. checkNew() is best-effort when driven by
pollInterval’s timer: a failed poll is swallowed and never touches isLoading/error —
a transient background failure must not blank an otherwise-working feed.
Inside a disabled provider (enabled={false}) the hook is inert: empty activities,
isLoading: false, error: null, enabled: false, all functions no-op resolving
undefined, zero network — initialData and polling are ignored too.
Extended by
Section titled “Extended by”Type Parameters
Section titled “Type Parameters”| Type Parameter | Default type |
|---|---|
TCustom |
Record<string, unknown> |
Properties
Section titled “Properties”initialData?
Section titled “initialData?”optional initialData?: FeedPage<TCustom> | Page<Activity<TCustom>>;optional live?: boolean;Keep this feed fresh. Two cadences, both paused while the tab is hidden and both firing immediately on return:
- every 5s, a head check — one Redis read that answers “did a new activity arrive”, costing a full page read only when it did;
- every
liveRevalidateInterval, the changes a head can never report: edits to the activities on screen, and re-reads of the objects they point at.
liveObjectsInterval?
Section titled “liveObjectsInterval?”optional liveObjectsInterval?: number;liveObjectsMaxRefs?
Section titled “liveObjectsMaxRefs?”optional liveObjectsMaxRefs?: number;Ceiling on how many refs one object sweep reads, across all its requests. Default 200; every 100 refs is one request per tick.
A deeply scrolled feed can hold thousands of refs, and refreshing all of them every interval would cost more than the per-card polling this replaces. Past the ceiling the NEWEST activities are refreshed and the tail keeps its last-read values — stale rather than expensive, warned once in the console rather than silently.
liveRevalidateInterval?
Section titled “liveRevalidateInterval?”optional liveRevalidateInterval?: number;Milliseconds between revalidation ticks under live. Default 30000; 0 disables
revalidation entirely, leaving only the 5s new-activity check.
Deliberately slower than the head cadence. A head check is one Redis read that usually answers “nothing new”; a revalidation is a real page read plus a batch object read, and edits and object updates happen a handful of times a day, not a handful of times a minute. Running both at 5s would bill six times the requests for the same information.
onPromotedClick?
Section titled “onPromotedClick?”optional onPromotedClick?: (promoted) => void;Invoked by the returned trackPromotedClick.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
promoted |
PromotedActivity<TCustom> |
Returns
Section titled “Returns”void
onPromotedImpression?
Section titled “onPromotedImpression?”optional onPromotedImpression?: (promoted, ctx) => void;Fired once per PLACED SLOT — when a promoted row enters items, not when it
enters the viewport (which we cannot see from here). Wire an
IntersectionObserver yourself if you need true viewability; this is the hook
for sending an event to your own analytics.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
promoted |
PromotedActivity<TCustom> |
ctx |
{ slot: number; } |
ctx.slot |
number |
Returns
Section titled “Returns”void
pageSize?
Section titled “pageSize?”optional pageSize?: number;Rows per request, for every read this hook makes (first page, loadNext,
refresh, checkNew). Default 20. Raise it for infinite scroll on a desktop
viewport, where 20 rows can be a single screen and each scroll costs a round trip.
newCount saturates at this value — see checkNew below.
pollInterval?
Section titled “pollInterval?”optional pollInterval?: number;promotedPosition?
Section titled “promotedPosition?”optional promotedPosition?: number;Activities before the first promoted slot in items. Default 3.
promotedRepeatEvery?
Section titled “promotedRepeatEvery?”optional promotedRepeatEvery?: number | null;Activities between promoted slots. Omit (or null) to place exactly once.