developmentseed / developmentseed/stac-react

Add Infinite Query Support for Pagination

Offen
#39 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
TypeScript
Sterne
34
Forks
4
PR-Merge-Kennzahlen
Keine gemergten PRs in 30 T.

Beschreibung

## 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

Beitragsleitfaden

Für dieses Repository ist kein Beitragsleitfaden indexiert

Rechercherichtung

Beginne damit, den Einstiegspunkt useStacSearch zu finden und zu prüfen, wie die Paginierung derzeit Ergebnisse ersetzt. Bewerte dann die vorgeschlagene useInfiniteQuery-API, die Seitenakkumulation, die linkbasierte Navigation sowie die Anforderungen an Tests und Dokumentation; abgeschlossen bedeutet, dass die aufgeführten Such- und Collections-Verhaltensweisen abgedeckt sind, ohne die bestehende Paginierung zu ändern.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
react, typescript
Bereich
api, frontend
Issue-Typ
Feature
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
35/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.