Skip to content

@scrollstackjs/core

The engine. Zero runtime dependencies, 1.92 KB gzipped, no framework knowledge.

ts
import { createInfiniteScroll } from '@scrollstackjs/core'

createInfiniteScroll

ts
function createInfiniteScroll<TData, TPageParam = number>(
  options: InfiniteScrollOptions<TData, TPageParam>,
): InfiniteScroll<TData, TPageParam>

TData is whatever one page looks like; TPageParam is your cursor type. Throws ScrollStackError if fetchPage, getNextPageParam, or initialPageParam is missing.

InfiniteScrollOptions

Required

OptionTypeDescription
initialPageParamTPageParamParam used for the very first fetch.
fetchPage(ctx: FetchPageContext<TPageParam>) => TData | Promise<TData>Fetches one page. May be sync.
getNextPageParamGetNextPageParam<TData, TPageParam>Derives the next param; null/undefined ends the list.

Retry

OptionTypeDefault
retryboolean | number | (failureCount: number, error: unknown) => boolean3
retryDelaynumber | (failureCount: number, error: unknown) => numbermin(1000 * 2 ** (attempt - 1), 30_000)

failureCount is 1 on the first failure. true retries forever, false never.

Observer

OptionTypeDefaultDescription
autoLoadbooleantrueWhether intersection triggers a load at all.
rootElement | Document | nullnull (viewport)IntersectionObserver root.
rootMarginstringMargin around the root: 'top right bottom left'.
thresholdnumber | readonly number[]Intersection ratio(s).

All three observer options are passed straight through to IntersectionObserver. See Horizontal & scoped scrolling for when root earns its keep.

Lifecycle

OptionSignatureFires
onLoadStart({ pageParam }) => voidbefore each fetch
onSuccess({ page, pageParam, pages }) => voidafter a page resolves
onError({ error, pageParam }) => voidafter retries are exhausted
pluginsreadonly ScrollStackPlugin[]run once, at creation

FetchPageContext

ts
interface FetchPageContext<TPageParam> {
  readonly pageParam: TPageParam
  readonly signal: AbortSignal // aborts on supersede, reset, or destroy
}

Forward signal to fetch so cancellations cancel real network work.

GetNextPageParam

ts
type GetNextPageParam<TData, TPageParam> = (
  lastPage: TData,
  allPages: readonly TData[],
  lastPageParam: TPageParam,
  allPageParams: readonly TPageParam[],
) => TPageParam | null | undefined

Only null and undefined end the list — 0 and '' are valid params. See Pagination.

InfiniteScroll

The returned engine.

MethodSignatureNotes
getSnapshot() => InfiniteScrollSnapshotSame reference until state changes.
subscribe(listener: () => void) => () => voidReturns unsubscribe.
on(event, handler) => () => voidReturns unsubscribe.
loadNextPage() => Promise<void>No-ops while fetching or when exhausted.
retry() => Promise<void>Clears failureCount and error, then fetches.
reset() => voidAborts in flight; back to initial state.
observeTarget(target: Element) => voidReplaces any previous target. SSR no-op.
destroyObserver() => voidStops observing; engine stays usable.
destroy() => voidAborts, disconnects, runs plugin cleanups. Terminal.

The control methods are bound — destructuring them is safe:

ts
const { loadNextPage, reset } = scroll

InfiniteScrollSnapshot

ts
interface InfiniteScrollSnapshot<TData, TPageParam = number> {
  readonly status: 'idle' | 'pending' | 'success' | 'error'
  readonly fetchStatus: 'idle' | 'fetching'
  readonly pages: readonly TData[]
  readonly pageParams: readonly TPageParam[]
  readonly error: unknown
  readonly hasNextPage: boolean
  readonly failureCount: number

  readonly isIdle: boolean // status === 'idle'
  readonly isLoading: boolean // status === 'pending' — first page, no data
  readonly isSuccess: boolean
  readonly isError: boolean // first load failed, nothing to show
  readonly isFetching: boolean
  readonly isFetchingNextPage: boolean // fetching && pages.length > 0
}

hasNextPage starts true — before the first fetch the engine assumes a page exists. error is set on any failure, including load-more failures where status stays 'success'; it's cleared when the next attempt starts.

Events

ts
interface ScrollStackEventMap<TData, TPageParam> {
  loadStart: { readonly pageParam: TPageParam }
  success: {
    readonly page: TData
    readonly pageParam: TPageParam
    readonly pages: readonly TData[]
  }
  error: { readonly error: unknown; readonly pageParam: TPageParam }
  reset: void
}

ScrollStackPlugin

ts
type ScrollStackPlugin<TData, TPageParam = number> = (
  instance: InfiniteScroll<TData, TPageParam>,
) => void | (() => void) // returned function runs on destroy()

See Events & plugins.

Trigger

The observer seam. Core ships one implementation and depends only on the contract, so alternative triggers (scroll events, manual, mutation-based) can live in their own packages.

ts
interface Trigger {
  observe(target: Element): void
  disconnect(): void
}

function createIntersectionTrigger(options: IntersectionTriggerOptions): Trigger | null // null when there is no IntersectionObserver

Errors

ts
class ScrollStackError extends Error {}

Thrown synchronously for misconfiguration — missing required options, or an observeTarget argument that isn't an Element. Fetch failures never surface as ScrollStackError; they land in snapshot.error as whatever your fetchPage threw.

Also exported

ExportPurpose
createEmitterThe tiny typed emitter core uses internally.
DEFAULT_RETRY / DEFAULT_RETRY_DELAYThe defaults, if you want to extend rather than replace them.
resolveRetry / resolveRetryDelayNormalize a RetryValue/RetryDelayValue to a decision.

Types: FetchPageContext, GetNextPageParam, RetryValue, RetryDelayValue, InfiniteScrollOptions, InfiniteScrollSnapshot, InfiniteScroll, ScrollStatus, FetchStatus, ScrollStackEventMap, ScrollStackPlugin, Trigger, IntersectionTriggerOptions, Emitter, EventMap.