developmentseed / developmentseed/stac-react

Add Infinite Query Support for Pagination

Aperta
#39 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
Lingua principale
TypeScript
Stelle
34
Fork
4
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

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

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Direzione di ricerca

Inizia individuando il punto di ingresso useStacSearch e verificando come la paginazione sostituisce attualmente i risultati. Valuta quindi l’API useInfiniteQuery proposta, l’accumulo delle pagine, la navigazione basata sui link e i requisiti relativi a test e documentazione; il lavoro è considerato completato quando i comportamenti elencati di ricerca e collections sono coperti senza modificare la paginazione esistente.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
react, typescript
Ambito
api, frontend
Tipo di issue
Funzionalità
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
35/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.