From da0fe9ca79f3b91a66e8bfa428f529e9603cfff4 Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Wed, 3 Dec 2025 15:25:34 -0500 Subject: [PATCH 1/2] add pagination for useCollections --- src/hooks/useCollections.ts | 96 ++++++++++++++++++++++++++++++------- src/stac-api/index.ts | 28 +++++++++-- src/types/stac.d.ts | 4 ++ 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/src/hooks/useCollections.ts b/src/hooks/useCollections.ts index bd2d12e..dcc626b 100644 --- a/src/hooks/useCollections.ts +++ b/src/hooks/useCollections.ts @@ -1,14 +1,18 @@ import { useCallback, useEffect, useState, useMemo } from 'react'; import { type ApiError, type LoadingState } from '../types'; -import type { CollectionsResponse } from '../types/stac'; +import type { CollectionsPayload, CollectionsResponse, Link /*LinkBody*/ } from '../types/stac'; import debounce from '../utils/debounce'; import { useStacApiContext } from '../context'; +type PaginationHandler = () => void; + type StacCollectionsHook = { - collections?: CollectionsResponse, - reload: () => void, - state: LoadingState - error?: ApiError + collections?: CollectionsResponse; + reload: () => void; + state: LoadingState; + error?: ApiError; + nextPage: PaginationHandler | undefined + previousPage: PaginationHandler | undefined }; function useCollections(): StacCollectionsHook { @@ -16,25 +20,81 @@ function useCollections(): StacCollectionsHook { const [ state, setState ] = useState('IDLE'); const [ error, setError ] = useState(); + const [ nextPageConfig, setNextPageConfig ] = useState(); + const [ previousPageConfig, setPreviousPageConfig ] = useState(); + + /** + * Extracts the pagination config from the the links array of the items response + */ + const setPaginationConfig = useCallback( + (links: Link[]) => { + setNextPageConfig(links.find(({ rel }) => rel === 'next')); + setPreviousPageConfig(links.find(({ rel }) => ['prev', 'previous'].includes(rel))); + }, [] + ); + + /** + * Resets the state and processes the results from the provided request + */ + const processRequest = useCallback((request: Promise) => { + setState('LOADING'); + setError(undefined); + setNextPageConfig(undefined); + setPreviousPageConfig(undefined); + + request + .then(response => response.json()) + .then(data => { + setCollections(data); + if (data.links) { + setPaginationConfig(data.links); + } + }) + .catch((err) => setError(err)) + .finally(() => setState('IDLE')); + }, [setPaginationConfig, setCollections]); + const _getCollections = useCallback( - () => { + (payload?: CollectionsPayload) => { if (stacApi) { - setState('LOADING'); - - stacApi.getCollections() - .then(response => response.json()) - .then(setCollections) - .catch((err) => { - setError(err); - setCollections(undefined); - }) - .finally(() => setState('IDLE')); + processRequest(stacApi.getCollections(payload)); } }, - [setCollections, stacApi] + [stacApi, processRequest] ); const getCollections = useMemo(() => debounce(_getCollections), [_getCollections]); + /** + * Retreives a page from a paginatied item set using the provided link config. + * Executes a POST request against the `search` endpoint if pagination uses POST + * or retrieves the page items using GET against the link href + */ + + const flipPage = useCallback( + (config?: any) => { + if (config.href) { + const url = new URL(config.href); + const params = url.searchParams; + const urlParams = Object.fromEntries(params.entries()); + const payload = { + ...urlParams, + }; + getCollections(payload); + } + }, + [getCollections] + ); + + const nextPageFn = useCallback( + () => flipPage(nextPageConfig), + [flipPage, nextPageConfig] + ); + + const previousPageFn = useCallback( + () => flipPage(previousPageConfig), + [flipPage, previousPageConfig] + ); + useEffect( () => { if (stacApi && !error && !collections) { @@ -49,6 +109,8 @@ function useCollections(): StacCollectionsHook { reload: getCollections, state, error, + nextPage: nextPageConfig ? nextPageFn : undefined, + previousPage: previousPageConfig ? previousPageFn : undefined }; } diff --git a/src/stac-api/index.ts b/src/stac-api/index.ts index 7531f76..c670091 100644 --- a/src/stac-api/index.ts +++ b/src/stac-api/index.ts @@ -1,7 +1,7 @@ import type { ApiError, GenericObject } from '../types'; -import type { Bbox, SearchPayload, DateRange } from '../types/stac'; +import type { Bbox, SearchPayload, DateRange, CollectionsPayload } from '../types/stac'; -type RequestPayload = SearchPayload; +type RequestPayload = SearchPayload | CollectionsPayload; type FetchOptions = { method?: string, payload?: RequestPayload, @@ -84,6 +84,21 @@ class StacApi { return new URLSearchParams(queryObj).toString(); } + collectionsPayloadToQuery(payload: CollectionsPayload): string { + const queryObj = {}; + for (const [key, value] of Object.entries(payload)) { + if (!value) continue; + + if (Array.isArray(value)) { + queryObj[key] = value.join(','); + } else { + queryObj[key] = value; + } + } + + return new URLSearchParams(queryObj).toString(); + } + async handleError(response: Response) { const { status, statusText } = response; const e: ApiError = { @@ -146,8 +161,13 @@ class StacApi { } } - getCollections(): Promise { - return this.fetch(`${this.baseUrl}/collections`); + getCollections(payload?: CollectionsPayload, headers = {}): Promise { + if (payload) { + const query = this.collectionsPayloadToQuery(payload); + return this.fetch(`${this.baseUrl}/collections?${query}`, { method: 'GET', headers }); + } else { + return this.fetch(`${this.baseUrl}/collections`); + } } get(href: string, headers = {}): Promise { diff --git a/src/types/stac.d.ts b/src/types/stac.d.ts index d33dd4d..b775130 100644 --- a/src/types/stac.d.ts +++ b/src/types/stac.d.ts @@ -21,6 +21,10 @@ export type SearchPayload = { sortby?: Sortby[] } +export type CollectionsPayload = { + offset?: number; +} + export type LinkBody = SearchPayload & { merge?: boolean } From 42858ae51f056ca771480f2eab21e4a77a125c23 Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Wed, 3 Dec 2025 16:27:18 -0500 Subject: [PATCH 2/2] evaluate stacApi url change --- src/hooks/useCollections.ts | 16 +++++++++++----- src/utils/usePreviousValue.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 src/utils/usePreviousValue.ts diff --git a/src/hooks/useCollections.ts b/src/hooks/useCollections.ts index dcc626b..f706a43 100644 --- a/src/hooks/useCollections.ts +++ b/src/hooks/useCollections.ts @@ -3,6 +3,8 @@ import { type ApiError, type LoadingState } from '../types'; import type { CollectionsPayload, CollectionsResponse, Link /*LinkBody*/ } from '../types/stac'; import debounce from '../utils/debounce'; import { useStacApiContext } from '../context'; +import StacApi from '../stac-api'; +import { usePreviousValue } from '../utils/usePreviousValue'; type PaginationHandler = () => void; @@ -11,8 +13,9 @@ type StacCollectionsHook = { reload: () => void; state: LoadingState; error?: ApiError; - nextPage: PaginationHandler | undefined - previousPage: PaginationHandler | undefined + nextPage: PaginationHandler | undefined; + previousPage: PaginationHandler | undefined; + stacApiEndpoint: StacApi | undefined; }; function useCollections(): StacCollectionsHook { @@ -23,6 +26,8 @@ function useCollections(): StacCollectionsHook { const [ nextPageConfig, setNextPageConfig ] = useState(); const [ previousPageConfig, setPreviousPageConfig ] = useState(); + const previousStacApi = usePreviousValue(stacApi); + /** * Extracts the pagination config from the the links array of the items response */ @@ -97,11 +102,11 @@ function useCollections(): StacCollectionsHook { useEffect( () => { - if (stacApi && !error && !collections) { + if ((stacApi && !error && !collections) || (previousStacApi && stacApi && (stacApi !== previousStacApi))) { getCollections(); } }, - [getCollections, stacApi, collections, error] + [getCollections, stacApi, collections, error, previousStacApi] ); return { @@ -110,7 +115,8 @@ function useCollections(): StacCollectionsHook { state, error, nextPage: nextPageConfig ? nextPageFn : undefined, - previousPage: previousPageConfig ? previousPageFn : undefined + previousPage: previousPageConfig ? previousPageFn : undefined, + stacApiEndpoint: stacApi }; } diff --git a/src/utils/usePreviousValue.ts b/src/utils/usePreviousValue.ts new file mode 100644 index 0000000..2115f95 --- /dev/null +++ b/src/utils/usePreviousValue.ts @@ -0,0 +1,9 @@ +import { useRef, useEffect } from 'react'; + +export const usePreviousValue = (value: T) => { + const ref = useRef(value); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +};