-
Notifications
You must be signed in to change notification settings - Fork 4
Open
Description
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 paginationProposed 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
- Pagination of Search Results #5 - Add Standalone Hooks (Context-Free Mode)
- Set up Dependabot #10 - Add Infinite Query Support
Testing Requirements
- Test automatic re-search on param changes
- Test with URL parameters
- Test enabled/disabled state
- Test with custom search links
- Test with and without StacApi context
- Test pagination link extraction
- Test error handling
- Test with all search parameter types
Documentation Requirements
- Document both stateful and declarative patterns
- Provide guidance on when to use each
- Show URL synchronization example
- Document infinite scroll pattern
- Show migration examples
- Explain trade-offs between approaches
Metadata
Metadata
Assignees
Labels
No labels