developmentseed / developmentseed/stac-react
Add Infinite Query Support for Pagination
- Dominant language
- TypeScript
- Stars
- 34
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
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
```typescript
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
```typescript
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 (
{hasNextPage && (
{isFetchingNextPage ? 'Loading...' : 'Load More'}
)}
);
}
```
## Use Cases from stac-map
stac-map uses `useInfiniteQuery` for both collections and search:
```typescript
// 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
```typescript
type UseStacSearchInfiniteOptions = {
/** Search parameters */
params: StacSearchParams;
/** Optional: specific search link to use */
searchLink?: Link;
/** Enable/disable search */
enabled?: boolean;
/** Custom headers */
headers?: Record;
};
type UseStacSearchInfiniteResult = {
/** All pages of results */
data?: {
pages: SearchResponse[];
pageParams: (Link | undefined)[];
};
/** All items flattened */
items?: Item[];
/** Fetch next page */
fetchNextPage: () => Promise;
/** Fetch previous page (if supported) */
fetchPreviousPage?: () => Promise;
/** 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;
};
function useStacSearchInfinite(
options: UseStacSearchInfiniteOptions
): UseStacSearchInfiniteResult;
```
### Implementation
```typescript
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
```typescript
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 (
{isFetchingNextPage && }
);
}
```
#### 2. Load More Button
```typescript
function LoadMoreResults() {
const {
items,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useStacSearchInfinite({
params: { bbox: [-180, -90, 180, 90], limit: 50 },
});
return (
Showing {items.length} items
{hasNextPage && (
fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
)}
);
}
```
#### 3. Virtualized List
```typescript
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedResults() {
const { items, fetchNextPage, hasNextPage } = useStacSearchInfinite({
params: { collections: ['landsat-8'] },
});
const parentRef = useRef(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 (
{virtualizer.getVirtualItems().map(item => (
))}
);
}
```
#### 4. Page Count Display
```typescript
function ResultsWithPageInfo() {
const { data, items, fetchNextPage, hasNextPage } = useStacSearchInfinite({
params: { collections: ['sentinel-1'] },
});
const pageCount = data?.pages.length ?? 0;
return (
Showing {items.length} items across {pageCount} pages
{hasNextPage && (
fetchNextPage()}>Load Page {pageCount + 1}
)}
);
}
```
## Collections Infinite Query
Similarly, add infinite query support for collections:
```typescript
function useCollectionsInfinite(
collectionsUrl?: string
): UseInfiniteQueryResult {
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
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.