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
- Initial fetch uses
initialPageParamas the page parameter getNextPageParamextracts the cursor from the last page – returnnullwhen no more pages- Call
fetchNextPage()to load the next page (appended) - 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:
| Option | Type | Description |
|---|---|---|
getNextPageParam | (lastPage, allPages) => T | null | Extract next cursor. Return null = no more pages. |
getPreviousPageParam | (firstPage, allPages) => T | null | Extract previous cursor. Optional. |
initialPageParam | T | Page param for the first fetch. |
maxPages | number | Maximum pages in memory. Oldest evicted. |

