Spoosh
Injects

injectPages

Bidirectional paginated data fetching with infinite scroll support using Signals

For usage patterns and examples, see the Infinite Queries guide.

Basic Usage

Given an API that returns:

{
  "items": [
    { "id": 1, "title": "Post 1" },
    { "id": 2, "title": "Post 2" }
  ],
  "meta": { "page": 1, "hasMore": true }
}
@Component({
  selector: "app-post-list",
  template: `
    @if (posts.loading()) {
      <div>Loading...</div>
    } @else {
      @for (post of posts.data(); track post.id) {
        <app-post-card [post]="post" />
      }

      @if (posts.canFetchNext()) {
        <button (click)="posts.fetchNext()" [disabled]="posts.fetchingNext()">
          {{ posts.fetchingNext() ? "Loading..." : "Load More" }}
        </button>
      }
    }
  `,
})
export class PostListComponent {
  posts = injectPages(
    (api) => api("posts").GET({ query: { page: 1, limit: 20 } }),
    {
      canFetchNext: ({ lastPage }) => lastPage?.data?.meta.hasMore ?? false,
      nextPageRequest: ({ lastPage }) => ({
        query: { page: (lastPage?.data?.meta.page ?? 0) + 1 },
      }),
      merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
    }
  );
}

Options

OptionTypeRequiredDescription
merger(pages) => TItem[]YesMerge all pages into items
canFetchNext(ctx) => booleanNoCheck if next page exists. Default: () => false
nextPageRequest(ctx) => Partial<TRequest>NoBuild request for next page
canFetchPrev(ctx) => booleanNoCheck if previous page exists
prevPageRequest(ctx) => Partial<TRequest>NoBuild request for previous page
enabledboolean | Signal<bool>NoWhether to fetch automatically

Context Object

// For canFetchNext and nextPageRequest
type NextContext<TData, TRequest> = {
  lastPage: InfinitePage<TData> | undefined;
  pages: InfinitePage<TData>[];
  request: TRequest;
};

// For canFetchPrev and prevPageRequest
type PrevContext<TData, TRequest> = {
  firstPage: InfinitePage<TData> | undefined;
  pages: InfinitePage<TData>[];
  request: TRequest;
};

// Each page in the pages array
type InfinitePage<TData> = {
  status: "pending" | "loading" | "success" | "error" | "stale";
  data?: TData;
  error?: TError;
  meta?: TMeta;
  input?: { query?; params?; body? };
};

Returns

PropertyTypeDescription
dataSignal<TItem[] | undefined>Merged items from all pages
pagesSignal<InfinitePage<TData>[]>Array of all pages with status, data, and meta
loadingSignal<boolean>True during initial load
fetchingSignal<boolean>True during any fetch
fetchingNextSignal<boolean>True while fetching next page
fetchingPrevSignal<boolean>True while fetching previous
canFetchNextSignal<boolean>Whether next page exists
canFetchPrevSignal<boolean>Whether previous page exists
fetchNext() => Promise<void>Fetch the next page
fetchPrev() => Promise<void>Fetch the previous page
trigger(options?) => Promise<void>Trigger fetch with optional new request options
abort() => voidAbort current request
errorSignal<TError | undefined>Error if request failed

On this page