Skip to content

useInfiniteFeed

function useInfiniteFeed<TCustom>(
group,
id,
opts?): object;

useFeed with the scroll wiring attached: everything that hook returns, plus a sentinelRef to put on a trailing element and an onEndReached for React Native.

const { activities, sentinelRef, isLoadingInitial, error, retry } = useInfiniteFeed('timeline', uid)
if (isLoadingInitial && activities.length === 0) return <FullPageLoading />
return <>
{activities.map((a) => <Row key={a.id} activity={a} />)}
{error && <button onClick={() => void retry()}>Try again</button>}
<div ref={sentinelRef} />
</>

The paging rules all live in useFeed — in-flight guard, id dedupe, the error guard that retry() clears. This adds only the two things a sentinel needs on top:

  1. It re-checks after every page. The sentinel does not move when a page lands, so no new intersection event fires and one-page-per-gesture would be the ceiling.
  2. It reads canLoadMore/loadNext through refs. The observer outlives the render that created it, so a captured closure would page against a stale cursor.

Where there is no IntersectionObserver (React Native, SSR), sentinelRef is an inert no-op and onEndReached — wired to FlatList’s prop of the same name — is the path.

Type Parameter Default type
TCustom Record<string, unknown>
Parameter Type
group string
id string
opts? UseInfiniteFeedOptions<TCustom>
activities: Activity<TCustom>[];
addActivity: (a) => Promise<Activity<TCustom> | undefined>;
Parameter Type Description
a { custom?: TCustom; foreign_id?: string | null; object: string; refs?: string[]; target?: string | null; time?: string; verb: string; } -
a.custom? TCustom -
a.foreign_id? string | null -
a.object string -
a.refs? string[] Objects this activity points at, as type:id. Max 4. Resolved into objects above.
a.target? string | null -
a.time? string -
a.verb string -

Promise<Activity<TCustom> | undefined>

canLoadMore: boolean;

Bind an infinite-scroll sentinel to THIS, not to hasNext: it folds in the two states where another fetch would be wrong — one already in flight, and an unacknowledged error on the same cursor.

checkNew: () => Promise<void>;

Promise<void>

enabled: boolean;
error: Error | null;
hasNext: boolean;
isLoading: boolean;
isLoadingInitial: boolean;
isLoadingMore: boolean;
items: FeedItem<TCustom>[];

activities with promoted rows interleaved per the placement props.

loadNext: () => Promise<void>;

Append the next page. Safe to call from an IntersectionObserver: it is a no-op at end-of-feed, while a page is already in flight, and while error is set.

The error guard is what stops a sentinel from hammering: a failed page leaves next unchanged, so an unguarded retry re-issues the identical request for as long as the sentinel stays intersecting — which, with the list short one page, is forever. Call retry() to clear the error and try that same cursor again.

Promise<void>

newCount: number = pending.length;
objects: Record<string, DropInObject<TCustom>>;

Refs sidecar for the currently-shown page(s), keyed type:id. {} when the server sent none — never undefined. Resolve an activity’s refs against it with resolveRefs(activity, objects).

onEndReached: () => void;

<FlatList onEndReached={onEndReached} onEndReachedThreshold={0.5} />. Guarded the same way as the sentinel, because FlatList fires it repeatedly near the end.

void

promoted: PromotedActivity<TCustom>[];

The eligible promoted set for this reader — NOT placed. Empty when none.

refresh: () => Promise<void>;

Promise<void>

retry: () => Promise<void>;

Clear the error and re-issue whatever failed. The explicit escape from loadNext’s error guard — wire it to a “Try again” button, never to the sentinel.

Which read it re-issues depends on what broke. A failed page 2+ has a cursor to ask for again, so it re-fetches that. A failed FIRST page never set one — next is still null — so a cursored refetch has nothing to send and would silently no-op, leaving the error terminal and a remount the only way out. That case delegates to refresh(), which re-reads page 1 uncursored. Defined after refresh for that reason.

next === null also means end-of-feed, where retry is meaningless: the hadError check keeps that a no-op rather than turning an exhausted feed into a silent refetch.

Promise<void>

revalidateObjects: () => Promise<void>;

Re-read the shown activities’ objects on demand, without touching pagination — see the JSDoc above the callback.

Promise<void>

sentinelRef: (node) => void;

Attach to a trailing element. A callback ref, not an object ref, so it also fires when the sentinel is conditionally unmounted and remounted.

Parameter Type
node Element | null

void

showNew: () => void;

void

trackPromotedClick: (p) => void;

Call from your row’s click handler; forwards to onPromotedClick.

Parameter Type
p PromotedActivity<TCustom>

void

updateActivity: (activityId, body, callOpts?) => Promise<Activity<TCustom> | undefined>;

Optimistic custom patch — see the JSDoc above the callback for the full reject-after-rollback / onError contract.

Parameter Type
activityId string
body PatchBody
callOpts? { onError?: OptimisticOnError; }
callOpts.onError? OptimisticOnError

Promise<Activity<TCustom> | undefined>