Skip to main content

Data Fetching

2 min read

Infinite Queries

Infinite queries accumulate pages instead of replacing data. Support cursor-based and offset-based pagination, bidirectional scrolling, and memory capping via maxPages.


Basic Infinite Query

import { createInfiniteQuery } from "@directive-run/query";

const feed = createInfiniteQuery({
  name: "feed",
  key: (facts) => facts.userId ? { userId: facts.userId } : null,
  fetcher: async (params, signal) => {
    const url = `/api/feed?user=${params.userId}&cursor=${params.pageParam ?? ""}`;
    const res = await fetch(url, { signal });
    return res.json();
  },
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  initialPageParam: null,
});

How It Works

  1. Initial fetch uses initialPageParam as the page parameter
  2. getNextPageParam extracts the cursor from the last page – return null when no more pages
  3. Call fetchNextPage() to load the next page (appended)
  4. Optionally use getPreviousPageParam + fetchPreviousPage() for bidirectional scrolling

InfiniteResourceState

Extends ResourceState with page management:

interface InfiniteResourceState<T> extends ResourceState<T[]> {
  pages: T[];
  pageParams: unknown[];
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  isFetchingNextPage: boolean;
  isFetchingPreviousPage: boolean;
}

Bidirectional Scrolling

const timeline = createInfiniteQuery({
  name: "timeline",
  key: (facts) => facts.userId ? { userId: facts.userId } : null,
  fetcher: async (params, signal) => api.getTimeline(params),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  initialPageParam: "now",
});

// Load newer posts
timeline.fetchNextPage(system.facts);
// Load older posts
timeline.fetchPreviousPage(system.facts);

Memory Management

Cap the number of pages kept in memory. Oldest pages are evicted when the limit is exceeded.

const feed = createInfiniteQuery({
  name: "feed",
  key: (facts) => facts.userId ? { userId: facts.userId } : null,
  fetcher: async (params, signal) => api.getFeed(params),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  initialPageParam: null,
  maxPages: 5, // keep only the 5 most recent pages
});

With createQuerySystem

const app = createQuerySystem({
  facts: { userId: "" },
  infiniteQueries: {
    feed: {
      key: (f) => f.userId ? { userId: f.userId } : null,
      fetcher: async (p, signal) => api.getFeed(p.userId, p.pageParam),
      getNextPageParam: (lastPage) => lastPage.nextCursor,
      initialPageParam: null,
      maxPages: 10,
    },
  },
});

app.facts.userId = "42";
await app.settle();

// Bound handles – no facts param
app.infiniteQueries.feed.fetchNextPage();
app.infiniteQueries.feed.fetchPreviousPage();
app.infiniteQueries.feed.refetch(); // resets to first page

const state = app.read("feed");
// { pages: [...], hasNextPage: true, isFetchingNextPage: false, ... }

Options

All options from createQuery are available, plus:

OptionTypeDescription
getNextPageParam(lastPage, allPages) => T | nullExtract next cursor. Return null = no more pages.
getPreviousPageParam(firstPage, allPages) => T | nullExtract previous cursor. Optional.
initialPageParamTPage param for the first fetch.
maxPagesnumberMaximum pages in memory. Oldest evicted.
Previous
Subscriptions

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management