Problem
The current useStacSearch hook is stateful with individual setters for each parameter:
const {
setBbox,
setCollections,
setDateRangeFrom,
setDateRangeTo,
setLimit,
setSortby,
submit,
results,
} = useStacSearch();
This pattern:
- Requires multiple calls to set up a search
- Doesn't work well with declarative React patterns
- Makes it difficult to sync with URL parameters
- Doesn't support passing complete search objects
- Requires explicit
submit() call
Many applications need a declarative search hook where search parameters are props that automatically trigger searches when they change.
Current Behavior
function SearchPage() {
const {
setBbox,
setCollections,
submit,
results
} = useStacSearch();
// Multi-step setup
useEffect(() => {
setBbox([-180, -90, 180, 90]);
setCollections(['collection-1']);
submit(); // Must explicitly submit
}, []);
return <Results data={results} />;
}
Desired Behavior
function SearchPage({ bbox, collections, datetime }) {
// Declarative - automatically searches when params change
const { results, isLoading, error } = useStacSearch({
bbox,
collections,
datetime,
limit: 10,
});
return <Results data={results} />;
}
Use Cases from stac-map
stac-map uses a declarative pattern:
// Search parameters and link are passed directly
const searchQuery = useStacSearch(
{ collections, bbox, datetime }, // Search params
searchLink // Link from STAC API
);
// Automatically re-searches when params change
// Returns useInfiniteQuery for pagination
Proposed Solution
New Declarative Hook
Add a new declarative variant alongside the existing stateful one:
type StacSearchParams = {
ids?: string[];
bbox?: Bbox;
collections?: string[];
datetime?: string;
limit?: number;
sortby?: Sortby[];
query?: Record<string, any>; // CQL2 queries
};
type UseStacSearchDeclarativeOptions = {
/** Search parameters */
params: StacSearchParams;
/** Optional: specific search link to use */
searchLink?: Link;
/** Enable/disable search */
enabled?: boolean;
/** Custom headers */
headers?: Record<string, string>;
};
type UseStacSearchDeclarativeResult = {
/** Search results */
results?: SearchResponse;
/** Loading state */
isLoading: boolean;
isFetching: boolean;
/** Error state */
error?: ApiErrorType;
/** Refetch with same params */
refetch: () => Promise<void>;
/** Pagination (if link-based pagination) */
nextPage?: () => void;
previousPage?: () => void;
hasNextPage?: boolean;
hasPreviousPage?: boolean;
};
function useStacSearchDeclarative(
options: UseStacSearchDeclarativeOptions
): UseStacSearchDeclarativeResult;
Implementation
import { useQuery } from '@tanstack/react-query';
import { useStacApiContext } from '../context/useStacApiContext';
function useStacSearchDeclarative({
params,
searchLink,
enabled = true,
headers = {},
}: UseStacSearchDeclarativeOptions): UseStacSearchDeclarativeResult {
const { stacApi } = useStacApiContext();
const { data, error, isLoading, isFetching, refetch } = useQuery({
queryKey: ['stac-search-declarative', params, searchLink?.href],
queryFn: async () => {
if (searchLink) {
// Use provided search link
return fetchViaLink(searchLink, params);
} else if (stacApi) {
// Use StacApi instance
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');
}
},
enabled: enabled && (!!stacApi || !!searchLink),
retry: false,
});
// Extract pagination links
const nextLink = data?.links?.find(l => l.rel === 'next');
const prevLink = data?.links?.find(l => ['prev', 'previous'].includes(l.rel));
return {
results: data,
isLoading,
isFetching,
error,
refetch,
hasNextPage: !!nextLink,
hasPreviousPage: !!prevLink,
};
}
async function fetchViaLink(link: Link, params: StacSearchParams) {
const url = new URL(link.href);
if (link.method === 'POST' || link.body) {
// POST request
return fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...link.headers,
},
body: JSON.stringify({ ...link.body, ...params }),
}).then(r => r.json());
} else {
// GET request
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.set(
key,
Array.isArray(value) ? value.join(',') : String(value)
);
}
});
return fetch(url.toString()).then(r => r.json());
}
}
Example Usage Patterns
1. Simple Declarative Search
function SimpleSearch() {
const [collections, setCollections] = useState(['landsat-8']);
const [bbox, setBbox] = useState<Bbox>();
const { results, isLoading } = useStacSearchDeclarative({
params: { collections, bbox, limit: 50 },
});
// Automatically re-searches when collections or bbox change
return (
<div>
<CollectionSelector value={collections} onChange={setCollections} />
<BboxSelector value={bbox} onChange={setBbox} />
{isLoading ? <Spinner /> : <Results items={results?.features} />}
</div>
);
}
2. URL-Synced Search
function UrlSyncedSearch() {
const [searchParams, setSearchParams] = useSearchParams();
const params = useMemo(() => ({
collections: searchParams.get('collections')?.split(','),
bbox: searchParams.get('bbox')?.split(',').map(Number) as Bbox,
datetime: searchParams.get('datetime'),
}), [searchParams]);
const { results } = useStacSearchDeclarative({ params });
// Search params stay in sync with URL
return <Results items={results?.features} />;
}
3. With Custom Search Link
function CustomEndpointSearch({ searchLink, bbox }) {
const { results, isLoading } = useStacSearchDeclarative({
params: { bbox, limit: 100 },
searchLink, // Use specific search endpoint
});
return <div>{/* ... */}</div>;
}
4. Conditional Search
function ConditionalSearch({ enabled, collections }) {
const { results } = useStacSearchDeclarative({
params: { collections },
enabled, // Only search when enabled is true
});
// Useful for not searching until user clicks "Search"
}
Coexistence with Stateful Hook
Both patterns should coexist:
// Stateful (existing) - for interactive search forms
export { useStacSearch } from './hooks/useStacSearch';
// Declarative (new) - for declarative patterns
export { useStacSearchDeclarative } from './hooks/useStacSearchDeclarative';
Users choose based on their needs:
- Stateful: Building search forms with stepwise input
- Declarative: URL-synced search, controlled components, derived state
Infinite Scroll Support
For infinite scroll/pagination:
function useStacSearchInfinite(options: UseStacSearchDeclarativeOptions) {
return useInfiniteQuery({
queryKey: ['stac-search-infinite', options.params],
queryFn: ({ pageParam }) => {
// Fetch using pageParam (next link)
},
initialPageParam: options.searchLink,
getNextPageParam: (lastPage) =>
lastPage.links?.find(l => l.rel === 'next'),
});
}
Benefits
- ✅ Declarative API matches React patterns
- ✅ Automatic re-search on parameter changes
- ✅ Easy URL parameter synchronization
- ✅ Simpler testing and reasoning
- ✅ Works with or without context
- ✅ Maintains backward compatibility
- ✅ Supports custom search endpoints
Breaking Changes
None - this adds a new hook alongside the existing one.
Migration Path
Existing code using stateful useStacSearch continues to work. New code can adopt useStacSearchDeclarative when it fits better.
Related Issues
Testing Requirements
Documentation Requirements
Problem
The current
useStacSearchhook is stateful with individual setters for each parameter:This pattern:
submit()callMany applications need a declarative search hook where search parameters are props that automatically trigger searches when they change.
Current Behavior
Desired Behavior
Use Cases from stac-map
stac-map uses a declarative pattern:
Proposed Solution
New Declarative Hook
Add a new declarative variant alongside the existing stateful one:
Implementation
Example Usage Patterns
1. Simple Declarative Search
2. URL-Synced Search
3. With Custom Search Link
4. Conditional Search
Coexistence with Stateful Hook
Both patterns should coexist:
Users choose based on their needs:
Infinite Scroll Support
For infinite scroll/pagination:
Benefits
Breaking Changes
None - this adds a new hook alongside the existing one.
Migration Path
Existing code using stateful
useStacSearchcontinues to work. New code can adoptuseStacSearchDeclarativewhen it fits better.Related Issues
Testing Requirements
Documentation Requirements