Add Infinite Query Support for Pagination
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- react, typescript
Research direction
Start by locating the useStacSearch entry point and reviewing how pagination currently replaces results. Then assess the proposed useInfiniteQuery API, page accumulation, link-based navigation, and the testing and documentation requirements; done means the listed search and collections behaviors are covered without changing existing pagination.
Written by the indexing model from the issue text.
Description
Problem
Current pagination in useStacSearch uses next/previous page functions that replace the current results. This doesn't support:
- Infinite scroll UI patterns
- "Load more" buttons
- Accumulating results across pages
- Automatic pagination as user scrolls
Modern STAC viewers need infinite scroll to display large result sets without traditional pagination controls.
Current Behavior
const { results, nextPage, previousPage } = useStacSearch();
// results contains only the current page
// nextPage() replaces results with next page
// Can't accumulate results for infinite scroll
Desired Behavior
function InfiniteResults() {
const {
data, // All pages combined
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useStacSearchInfinite({
params: { collections: ['landsat-8'] },
});
// Flatten all pages
const allItems = data?.pages.flatMap(page => page.features) ?? [];
return (
<div>
<ItemsGrid items={allItems} />
{hasNextPage && (
<button onClick={fetchNextPage} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
Use Cases from stac-map
stac-map uses useInfiniteQuery for both collections and search:
// Collections with infinite loading
const collectionsQuery = useInfiniteQuery({
queryKey: ['stac-collections', href],
queryFn: async ({ pageParam }) => fetchCollections(pageParam),
initialPageParam: href,
getNextPageParam: (lastPage) =>
lastPage?.links?.find(link => link.rel === 'next')?.href,
});
// Search with infinite loading
const searchQuery = useInfiniteQuery({
queryKey: ['search', search, link],
initialPageParam: updateLink(link, search),
getNextPageParam: (lastPage) =>
lastPage.links?.find(link => link.rel === 'next'),
queryFn: fetchSearch,
});
Proposed Solution
Hook Signature
type UseStacSearchInfiniteOptions = {
/** Search parameters */
params: StacSearchParams;
/** Optional: specific search link to use */
searchLink?: Link;
/** Enable/disable search */
enabled?: boolean;
/** Custom headers */
headers?: Record<string, string>;
};
type UseStacSearchInfiniteResult = {
/** All pages of results */
data?: {
pages: SearchResponse[];
pageParams: (Link | undefined)[];
};
/** All items flattened */
items?: Item[];
/** Fetch next page */
fetchNextPage: () => Promise<void>;
/** Fetch previous page (if supported) */
fetchPreviousPage?: () => Promise<void>;
/** Whether there are more pages */
hasNextPage: boolean;
hasPreviousPage?: boolean;
/** Loading states */
isLoading: boolean;
isFetchingNextPage: boolean;
isFetchingPreviousPage?: boolean;
/** Error state */
error?: ApiErrorType;
/** Refetch all pages */
refetch: () => Promise<void>;
};
function useStacSearchInfinite(
options: UseStacSearchInfiniteOptions
): UseStacSearchInfiniteResult;
Implementation
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
function useStacSearchInfinite({
params,
searchLink,
enabled = true,
headers = {},
}: UseStacSearchInfiniteOptions): UseStacSearchInfiniteResult {
const { stacApi } = useStacApiContext();
const query = useInfiniteQuery({
queryKey: ['stac-search-infinite', params, searchLink?.href],
queryFn: async ({ pageParam }) => {
if (pageParam) {
// Use pagination link
return await fetchViaLink(pageParam, params);
} else if (stacApi) {
// Initial search via StacApi
const response = await stacApi.search({
...params,
dateRange: params.datetime ? parseDateTime(params.datetime) : undefined,
}, headers);
if (!response.ok) {
throw new ApiError(
response.statusText,
response.status,
await response.text(),
response.url
);
}
return response.json();
} else {
throw new Error('Either provide stacApi context or searchLink');
}
},
initialPageParam: searchLink,
getNextPageParam: (lastPage: SearchResponse) => {
return lastPage.links?.find(link => link.rel === 'next');
},
getPreviousPageParam: (firstPage: SearchResponse) => {
return firstPage.links?.find(link =>
['prev', 'previous'].includes(link.rel)
);
},
enabled: enabled && (!!stacApi || !!searchLink),
retry: false,
});
// Flatten all items from all pages
const items = useMemo(() => {
return query.data?.pages.flatMap(page => page.features) ?? [];
}, [query.data]);
return {
data: query.data,
items,
fetchNextPage: query.fetchNextPage,
fetchPreviousPage: query.fetchPreviousPage,
hasNextPage: query.hasNextPage,
hasPreviousPage: query.hasPreviousPage,
isLoading: query.isLoading,
isFetchingNextPage: query.isFetchingNextPage,
isFetchingPreviousPage: query.isFetchingPreviousPage,
error: query.error,
refetch: query.refetch,
};
}
Example Usage Patterns
1. Infinite Scroll
function InfiniteScrollResults() {
const {
items,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useStacSearchInfinite({
params: { collections: ['sentinel-2'], limit: 25 },
});
const observerRef = useRef();
// Intersection observer for automatic loading
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 0.1 }
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
return (
<div>
<ItemsGrid items={items} />
<div ref={observerRef} style={{ height: 20 }} />
{isFetchingNextPage && <LoadingSpinner />}
</div>
);
}
2. Load More Button
function LoadMoreResults() {
const {
items,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useStacSearchInfinite({
params: { bbox: [-180, -90, 180, 90], limit: 50 },
});
return (
<div>
<ItemsGrid items={items} />
<p>Showing {items.length} items</p>
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
3. Virtualized List
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedResults() {
const { items, fetchNextPage, hasNextPage } = useStacSearchInfinite({
params: { collections: ['landsat-8'] },
});
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 200,
overscan: 5,
});
// Load more when near end
useEffect(() => {
const lastItem = virtualizer.getVirtualItems().at(-1);
if (lastItem && lastItem.index >= items.length - 1 && hasNextPage) {
fetchNextPage();
}
}, [virtualizer.getVirtualItems(), fetchNextPage, hasNextPage, items.length]);
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(item => (
<div
key={item.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${item.start}px)`,
}}
>
<ItemCard item={items[item.index]} />
</div>
))}
</div>
</div>
);
}
4. Page Count Display
function ResultsWithPageInfo() {
const { data, items, fetchNextPage, hasNextPage } = useStacSearchInfinite({
params: { collections: ['sentinel-1'] },
});
const pageCount = data?.pages.length ?? 0;
return (
<div>
<p>
Showing {items.length} items across {pageCount} pages
</p>
<ItemsGrid items={items} />
{hasNextPage && (
<button onClick={() => fetchNextPage()}>Load Page {pageCount + 1}</button>
)}
</div>
);
}
Collections Infinite Query
Similarly, add infinite query support for collections:
function useCollectionsInfinite(
collectionsUrl?: string
): UseInfiniteQueryResult<CollectionsResponse> {
return useInfiniteQuery({
queryKey: ['stac-collections-infinite', collectionsUrl],
queryFn: async ({ pageParam }) => {
const url = pageParam || collectionsUrl;
if (!url) throw new Error('No collections URL provided');
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch collections: ${response.statusText}`);
}
return response.json();
},
initialPageParam: collectionsUrl,
getNextPageParam: (lastPage: CollectionsResponse) => {
return lastPage.links?.find(link => link.rel === 'next')?.href;
},
enabled: !!collectionsUrl,
});
}
Benefits
- ✅ Support infinite scroll patterns
- ✅ "Load more" functionality
- ✅ Accumulate results across pages
- ✅ Better UX for large result sets
- ✅ Automatic pagination via intersection observer
- ✅ Works with virtualized lists
- ✅ Maintains all loaded data in cache
Performance Considerations
- React Query manages all pages efficiently
- Each page is cached separately
- Refetching only updates stale pages
- Virtual scrolling recommended for very large result sets
Breaking Changes
None - this is a new hook alongside existing pagination.
Testing Requirements
- Test initial page load
- Test fetching next page
- Test fetching previous page (if supported)
- Test items flattening across pages
- Test with link-based pagination
- Test with token-based pagination
- Test hasNextPage detection
- Test error handling across pages
- Test refetch behavior
Documentation Requirements
- Document infinite scroll pattern
- Show load more button example
- Document intersection observer usage
- Show virtualized list integration
- Document performance best practices
- Show page count tracking
- Document when to use infinite vs standard pagination
- Dominant language
- TypeScript
- Stars
- 34
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from developmentseed/stac-react
-
Difficulty 3/5 1-2 days Newbie friendliness 55/100
-
Difficulty 5/5 Over a week Newbie friendliness 30/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
Difficulty 5/5 Over a week Newbie friendliness 30/100
-
Difficulty 5/5 Over a week Newbie friendliness 42/100
All issues in developmentseed/stac-react
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
danielmiessler/LifeOS#2218 ·