Table of Contents

Infinite Queries

Infinite queries accumulate pages of data fetched sequentially with a page parameter — a cursor, offset, or page number. They share the same cache, deduplication, stale-while-revalidate, and invalidation guarantees as regular queries, but instead of holding a single TData value they hold an ordered list of pages and expose FetchNextPage / FetchPreviousPage for navigation.

Creating an Infinite Query

Use IQueryClient.CreateInfiniteQuery:

IInfiniteQuery<TArgs, TData, TPageParam> query =
    queryClient.CreateInfiniteQuery(new InfiniteQueryOptions<TArgs, TData, TPageParam>
    {
        KeyFactory       = args  => ...,
        Fetcher          = (args, page, ct) => ...,
        InitialPageParam = ...,
        GetNextPageParam = info  => ...,
    });

TData is the type of a single page (e.g. List<PostDto>). TPageParam is the type of the cursor or page identifier (e.g. int, Guid, DateTimeOffset).

Required Options

KeyFactory — same as regular queries. Keys identify the cache entry; all observers with the same key share one InfiniteQuery instance.

KeyFactory = listId => ["posts", listId]

Fetcher — receives args, the current page param, and a CancellationToken:

Fetcher = (listId, page, ct) => api.GetPostsAsync(listId, page, PageSize, ct)

InitialPageParam — the page param passed to Fetcher on the very first fetch:

InitialPageParam = 1       // offset / page-number pagination
InitialPageParam = Guid.Empty  // cursor-based, "before all items"

GetNextPageParam — returns the param for the next page, or PageParam<TPageParam>.None to signal there are no more pages. Receives an InfinitePageInfo containing the last loaded page and all accumulated pages:

// Page-number: stop when we get fewer items than requested
GetNextPageParam = info =>
    info.Page.Count < PageSize
        ? PageParam<int>.None
        : PageParam<int>.Some(info.PageParam + 1)

// Cursor: use the last item's id
GetNextPageParam = info =>
    info.Page.LastOrDefault()?.Id is { } id
        ? PageParam<Guid>.Some(id)
        : PageParam<Guid>.None

Optional Options

Option Default Description
GetPreviousPageParam null Enables FetchPreviousPage. Return None when at the start.
MaxPages null (unbounded) Trims oldest pages when exceeded on FetchNextPage; trims newest on FetchPreviousPage.
StaleTime global Stale time for Invalidate() no-op check.
CacheTime global Time to keep the entry after the last subscriber leaves.
RefetchInterval null Automatic polling (triggers RefetchAll).
RetryHandler global Custom retry logic.
IsEnabled true When false, no fetches run.
DataComparer null Per-page equality check to suppress duplicate emissions.
InitialData null Pre-seeds the first page. The query starts in Success state and immediately displays the seeded data while a background refetch refreshes it.
Name null Low-cardinality name used to tag metrics. Falls back to the first part of the QueryKey. See Observability.

InitialData

InitialData lets you pre-populate the first page from a local cache, SSR payload, or any synchronous source. The query starts in Success state — no loading flash — and immediately triggers a background refetch to stay fresh.

public readonly IInfiniteQuery<Guid, List<PostDto>, int> PostsQuery =
    queryClient.CreateInfiniteQuery(new InfiniteQueryOptions<Guid, List<PostDto>, int>
    {
        KeyFactory       = boardId => ["posts", boardId],
        Fetcher          = (boardId, page, ct) => api.GetPostsAsync(boardId, page, PageSize, ct),
        InitialPageParam = 1,
        GetNextPageParam = info =>
            info.Page.Count < PageSize ? PageParam<int>.None : PageParam<int>.Some(info.PageParam + 1),
        InitialData      = cachedFirstPage,   // IReadOnlyList<PostDto> from local storage / SSR
    });

Only the first page is seeded. Subsequent pages must be fetched via FetchNextPage. The seeded page uses InitialPageParam as its page parameter.

PageParam<TPageParam>

PageParam<TPageParam> is a small discriminated union that avoids the C# generic nullable ambiguity for value-type page params.

PageParam<int>.None        // no more pages
PageParam<int>.Some(2)     // next page param is 2
(PageParam<int>)2          // implicit conversion — equivalent to Some(2)

Fetching Pages

Initial Load

Call SetArgs to provide the args. The first fetch uses InitialPageParam:

query.SetArgs(listId);

FetchNextPage

Appends the next page. The page param comes from GetNextPageParam applied to the last loaded page. No-op when HasNextPage is false or no pages have been loaded yet.

query.FetchNextPage();

During the fetch, IsFetchingNextPage is true and Status remains Success — existing pages stay visible.

FetchPreviousPage

Prepends the previous page. Requires GetPreviousPageParam to be configured. No-op when HasPreviousPage is false.

query.FetchPreviousPage();

Invalidate / Refetch

Both re-fetch all currently loaded pages sequentially (rebuilding Pages and PageParams from scratch), matching TanStack Query behavior. Existing pages are carried in the Fetching state for stale-while-revalidate display.

query.Invalidate(); // respects StaleTime
query.Refetch();    // always re-fetches all pages

Client-level invalidation targets infinite queries the same way as regular queries:

queryClient.Invalidate(["posts", listId]);
queryClient.Invalidate(key => key.ToString().StartsWith("posts"));

State

InfiniteQueryState<TData, TPageParam>

Every state transition emits an immutable InfiniteQueryState snapshot:

state.Status               // Idle | Fetching | Success | Failure
state.Pages                // IReadOnlyList<TData> — all loaded pages, oldest first
state.PageParams           // IReadOnlyList<TPageParam> — PageParams[i] fetched Pages[i]
state.Error                // Exception? — last fetch failure

state.HasNextPage          // true when GetNextPageParam returns Some(...)
state.HasPreviousPage      // true when GetPreviousPageParam returns Some(...)
state.IsFetchingNextPage   // true while FetchNextPage is in progress (Status stays Success)
state.IsFetchingPreviousPage // true while FetchPreviousPage is in progress

state.IsIdle               // Status == Idle
state.IsFetching           // Status == Fetching (full RefetchAll)
state.IsSuccess            // Status == Success
state.IsFailure            // Status == Failure
state.HasData              // Pages.Count > 0
state.HasError             // Error is not null

When IsFetching is true during a RefetchAll, the state still carries the previous Pages and PageParams so you can show stale content while rebuilding.

Subscribing

// All state transitions — replays current state to new subscribers
query.State.Subscribe(state =>
{
    var allItems = state.Pages.SelectMany(p => p).ToList();
    Render(allItems, state.HasNextPage, state.IsFetchingNextPage);
});

// Emits all pages after a full settle (not during IsFetchingNextPage/Previous)
query.Success.Subscribe(pages => Console.WriteLine($"Loaded {pages.Count} pages"));

// Exception on each failed fetch
query.Failure.Subscribe(e => ShowError(e));

// Final state after every fetch (success or failure)
query.Settled.Subscribe(_ => HideGlobalSpinner());

Blazor Components

<InfiniteTransition> and <InfiniteSuspense> in DotNetQuery.Blazor handle subscriptions and re-rendering automatically. Both receive the full InfiniteQueryState as the Content context so the template can access pages, page counts, and loading flags in one place.

Shows Content whenever at least one page is available — during background refetches and while loading more pages. Shows Loading only before the first page arrives.

<InfiniteTransition Query="_postsQuery">
    <Content Context="state">
        @foreach (var post in state.Pages.SelectMany(p => p))
        {
            <PostCard Post="post" />
        }

        @if (state.HasNextPage || state.IsFetchingNextPage)
        {
            <button @onclick="LoadMore" disabled="@state.IsFetchingNextPage">
                @(state.IsFetchingNextPage ? "Loading…" : "Load more")
            </button>
        }
    </Content>
    <Loading><LoadingSpinner /></Loading>
    <Failure Context="e"><ErrorMessage Message="@e.Message" /></Failure>
</InfiniteTransition>

<InfiniteSuspense>

Shows Content only when IsSuccess && HasData. Reverts to Loading during full background refetches (same strict behavior as <Suspense>).

<InfiniteSuspense Query="_postsQuery">
    <Content Context="state">
        @foreach (var post in state.Pages.SelectMany(p => p))
        {
            <PostCard Post="post" />
        }
    </Content>
    <Loading><LoadingSpinner /></Loading>
    <Failure Context="e"><ErrorMessage Message="@e.Message" /></Failure>
</InfiniteSuspense>
Scenario InfiniteSuspense InfiniteTransition
Initial load (no pages) Loading Loading
Full refetch (stale pages available) Loading Content
FetchNextPage in progress Content Content
Success Content Content
Failure (no pages) Failure Failure
Failure (has pages) Failure Content

Service/Facade Pattern

Keep infinite queries in a dedicated service class, consistent with regular queries:

public sealed class PostsService(IQueryClient queryClient, IPostsApi api) : IDisposable
{
    private const int PageSize = 20;

    public readonly IInfiniteQuery<Guid, List<PostDto>, int> PostsQuery =
        queryClient.CreateInfiniteQuery(new InfiniteQueryOptions<Guid, List<PostDto>, int>
        {
            KeyFactory       = boardId => ["posts", boardId],
            Fetcher          = (boardId, page, ct) => api.GetPostsAsync(boardId, page, PageSize, ct),
            InitialPageParam = 1,
            GetNextPageParam = info =>
                info.Page.Count < PageSize ? PageParam<int>.None : PageParam<int>.Some(info.PageParam + 1),
            StaleTime = TimeSpan.FromMinutes(1),
        });

    public void Dispose() => PostsQuery.Dispose();
}
@page "/boards/{BoardId:guid}"
@inject PostsService Posts

<InfiniteTransition Query="Posts.PostsQuery">
    <Content Context="state">
        @foreach (var post in state.Pages.SelectMany(p => p))
        {
            <PostCard Post="post" />
        }
        @if (state.HasNextPage)
        {
            <button @onclick="() => Posts.PostsQuery.FetchNextPage()"
                    disabled="@state.IsFetchingNextPage">
                @(state.IsFetchingNextPage ? "Loading…" : "Load more")
            </button>
        }
    </Content>
    <Loading><p>Loading posts…</p></Loading>
    <Failure Context="e"><p>@e.Message</p></Failure>
</InfiniteTransition>

@code {
    [Parameter] public Guid BoardId { get; set; }

    protected override void OnParametersSet() => Posts.PostsQuery.SetArgs(BoardId);
}

Cleaning Up

Infinite queries implement IDisposable. Dispose the query (or the service that owns it) when done:

query.Dispose();

<InfiniteTransition> and <InfiniteSuspense> dispose their internal subscriptions automatically when the component is removed from the render tree.