11import { useCallback , useState , useMemo , useEffect } from 'react' ;
2+ import { useQuery , useQueryClient } from '@tanstack/react-query' ;
23import debounce from '../utils/debounce' ;
34import type { ApiError , LoadingState } from '../types' ;
45import type {
@@ -7,7 +8,6 @@ import type {
78 CollectionIdList ,
89 SearchPayload ,
910 SearchResponse ,
10- LinkBody ,
1111 Sortby ,
1212} from '../types/stac' ;
1313import { useStacApiContext } from '../context/useStacApiContext' ;
@@ -37,40 +37,62 @@ type StacSearchHook = {
3737 previousPage : PaginationHandler | undefined ;
3838} ;
3939
40+ type FetchRequest =
41+ | {
42+ type : 'search' ;
43+ payload : SearchPayload ;
44+ headers ?: Record < string , string > ;
45+ }
46+ | {
47+ type : 'get' ;
48+ url : string ;
49+ } ;
50+
4051function useStacSearch ( ) : StacSearchHook {
4152 const { stacApi } = useStacApiContext ( ) ;
42- const [ results , setResults ] = useState < SearchResponse > ( ) ;
53+ const queryClient = useQueryClient ( ) ;
54+
55+ // Search parameters state
4356 const [ ids , setIds ] = useState < string [ ] > ( ) ;
4457 const [ bbox , setBbox ] = useState < Bbox > ( ) ;
4558 const [ collections , setCollections ] = useState < CollectionIdList > ( ) ;
4659 const [ dateRangeFrom , setDateRangeFrom ] = useState < string > ( '' ) ;
4760 const [ dateRangeTo , setDateRangeTo ] = useState < string > ( '' ) ;
4861 const [ limit , setLimit ] = useState < number > ( 25 ) ;
4962 const [ sortby , setSortby ] = useState < Sortby [ ] > ( ) ;
50- const [ state , setState ] = useState < LoadingState > ( 'IDLE' ) ;
51- const [ error , setError ] = useState < ApiError > ( ) ;
63+
64+ // Track the current request (search or pagination) for React Query
65+ const [ currentRequest , setCurrentRequest ] = useState < FetchRequest | null > ( null ) ;
5266
5367 const [ nextPageConfig , setNextPageConfig ] = useState < Link > ( ) ;
5468 const [ previousPageConfig , setPreviousPageConfig ] = useState < Link > ( ) ;
5569
5670 const reset = ( ) => {
57- setResults ( undefined ) ;
5871 setBbox ( undefined ) ;
5972 setCollections ( undefined ) ;
6073 setIds ( undefined ) ;
6174 setDateRangeFrom ( '' ) ;
6275 setDateRangeTo ( '' ) ;
6376 setSortby ( undefined ) ;
6477 setLimit ( 25 ) ;
78+ setCurrentRequest ( null ) ;
79+ setNextPageConfig ( undefined ) ;
80+ setPreviousPageConfig ( undefined ) ;
6581 } ;
6682
6783 /**
6884 * Reset state when stacApi changes
6985 */
70- useEffect ( reset , [ stacApi ] ) ;
86+ useEffect ( ( ) => {
87+ if ( stacApi ) {
88+ reset ( ) ;
89+ // Invalidate all search queries when API changes
90+ void queryClient . invalidateQueries ( { queryKey : [ 'stacSearch' ] } ) ;
91+ }
92+ } , [ stacApi , queryClient ] ) ;
7193
7294 /**
73- * Extracts the pagination config from the the links array of the items response
95+ * Extracts the pagination config from the links array of the items response
7496 */
7597 const setPaginationConfig = useCallback ( ( links : Link [ ] ) => {
7698 setNextPageConfig ( links . find ( ( { rel } ) => rel === 'next' ) ) ;
@@ -93,85 +115,108 @@ function useStacSearch(): StacSearchHook {
93115 ) ;
94116
95117 /**
96- * Resets the state and processes the results from the provided request
118+ * Fetch function for searches using TanStack Query
97119 */
98- const processRequest = useCallback (
99- ( request : Promise < Response > ) => {
100- setResults ( undefined ) ;
101- setState ( 'LOADING' ) ;
102- setError ( undefined ) ;
103- setNextPageConfig ( undefined ) ;
104- setPreviousPageConfig ( undefined ) ;
105-
106- request
107- . then ( ( response ) => response . json ( ) )
108- . then ( ( data ) => {
109- setResults ( data ) ;
110- if ( data . links ) {
111- setPaginationConfig ( data . links ) ;
112- }
113- } )
114- . catch ( ( err ) => setError ( err ) )
115- . finally ( ( ) => setState ( 'IDLE' ) ) ;
116- } ,
117- [ setPaginationConfig ]
118- ) ;
120+ const fetchRequest = async ( request : FetchRequest ) : Promise < SearchResponse > => {
121+ if ( ! stacApi ) throw new Error ( 'No STAC API configured' ) ;
122+
123+ const response =
124+ request . type === 'search'
125+ ? await stacApi . search ( request . payload , request . headers )
126+ : await stacApi . get ( request . url ) ;
127+
128+ if ( ! response . ok ) {
129+ let detail ;
130+ try {
131+ detail = await response . json ( ) ;
132+ } catch {
133+ detail = await response . text ( ) ;
134+ }
135+ const err = Object . assign ( new Error ( response . statusText ) , {
136+ status : response . status ,
137+ statusText : response . statusText ,
138+ detail,
139+ } ) ;
140+ throw err ;
141+ }
142+ return await response . json ( ) ;
143+ } ;
119144
120145 /**
121- * Executes a POST request against the ` search` endpoint using the provided payload and headers
146+ * useQuery for search and pagination with caching
122147 */
123- const executeSearch = useCallback (
124- ( payload : SearchPayload , headers = { } ) =>
125- stacApi && processRequest ( stacApi . search ( payload , headers ) ) ,
126- [ stacApi , processRequest ]
127- ) ;
148+ const {
149+ data : results ,
150+ error,
151+ isLoading,
152+ isFetching,
153+ } = useQuery < SearchResponse , ApiError > ( {
154+ queryKey : [ 'stacSearch' , currentRequest ] ,
155+ queryFn : ( ) => fetchRequest ( currentRequest ! ) ,
156+ enabled : currentRequest !== null ,
157+ retry : false ,
158+ } ) ;
128159
129160 /**
130- * Execute a GET request against the provided URL
161+ * Extract pagination links from results
131162 */
132- const getItems = useCallback (
133- ( url : string ) => stacApi && processRequest ( stacApi . get ( url ) ) ,
134- [ stacApi , processRequest ]
135- ) ;
163+ useEffect ( ( ) => {
164+ // Only update pagination links when we have actual results with links
165+ // Don't clear them when results becomes undefined (during new requests)
166+ if ( results ?. links ) {
167+ setPaginationConfig ( results . links ) ;
168+ }
169+ } , [ results , setPaginationConfig ] ) ;
136170
137171 /**
138- * Retrieves a page from a paginated item set using the provided link config.
139- * Executes a POST request against the `search` endpoint if pagination uses POST
140- * or retrieves the page items using GET against the link href
172+ * Convert a pagination Link to a FetchRequest
141173 */
142- const flipPage = useCallback (
143- ( link ?: Link ) => {
144- if ( link ) {
145- let payload = link . body as LinkBody ;
146- if ( payload ) {
147- if ( payload . merge ) {
148- payload = {
149- ...payload ,
150- ...getSearchPayload ( ) ,
151- } ;
152- }
153- executeSearch ( payload , link . headers ) ;
154- } else {
155- getItems ( link . href ) ;
156- }
174+ const linkToRequest = useCallback (
175+ ( link : Link ) : FetchRequest => {
176+ if ( link . body ) {
177+ const payload = link . body . merge ? { ...link . body , ...getSearchPayload ( ) } : link . body ;
178+ return {
179+ type : 'search' ,
180+ payload,
181+ headers : link . headers ,
182+ } ;
157183 }
184+ return {
185+ type : 'get' ,
186+ url : link . href ,
187+ } ;
158188 } ,
159- [ executeSearch , getItems , getSearchPayload ]
189+ [ getSearchPayload ]
160190 ) ;
161191
162- const nextPageFn = useCallback ( ( ) => flipPage ( nextPageConfig ) , [ flipPage , nextPageConfig ] ) ;
163-
164- const previousPageFn = useCallback (
165- ( ) => flipPage ( previousPageConfig ) ,
166- [ flipPage , previousPageConfig ]
167- ) ;
192+ /**
193+ * Pagination handlers
194+ */
195+ const nextPageFn = useCallback ( ( ) => {
196+ if ( nextPageConfig ) {
197+ setCurrentRequest ( linkToRequest ( nextPageConfig ) ) ;
198+ }
199+ } , [ nextPageConfig , linkToRequest ] ) ;
200+
201+ const previousPageFn = useCallback ( ( ) => {
202+ if ( previousPageConfig ) {
203+ setCurrentRequest ( linkToRequest ( previousPageConfig ) ) ;
204+ }
205+ } , [ previousPageConfig , linkToRequest ] ) ;
168206
207+ /**
208+ * Submit handler for new searches
209+ */
169210 const _submit = useCallback ( ( ) => {
170211 const payload = getSearchPayload ( ) ;
171- executeSearch ( payload ) ;
172- } , [ executeSearch , getSearchPayload ] ) ;
212+ setCurrentRequest ( { type : 'search' , payload } ) ;
213+ } , [ getSearchPayload ] ) ;
214+
173215 const submit = useMemo ( ( ) => debounce ( _submit ) , [ _submit ] ) ;
174216
217+ // Sync loading state for backwards compatibility
218+ const state : LoadingState = isLoading || isFetching ? 'LOADING' : 'IDLE' ;
219+
175220 return {
176221 submit,
177222 ids,
@@ -186,7 +231,7 @@ function useStacSearch(): StacSearchHook {
186231 setDateRangeTo,
187232 results,
188233 state,
189- error,
234+ error : error ?? undefined ,
190235 sortby,
191236 setSortby,
192237 limit,
0 commit comments