developmentseed / developmentseed/stac-react
Add Flexible Search Hook
- Dominant language
- TypeScript
- Stars
- 34
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
## Problem
The current `useStacSearch` hook is stateful with individual setters for each parameter:
```typescript
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
```typescript
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 ;
}
```
## Desired Behavior
```typescript
function SearchPage({ bbox, collections, datetime }) {
// Declarative - automatically searches when params change
const { results, isLoading, error } = useStacSearch({
bbox,
collections,
datetime,
limit: 10,
});
return ;
}
```
## Use Cases from stac-map
stac-map uses a declarative pattern:
```typescript
// 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:
```typescript
type StacSearchParams = {
ids?: string[];
bbox?: Bbox;
collections?: string[];
datetime?: string;
limit?: number;
sortby?: Sortby[];
query?: Record; // 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;
};
type UseStacSearchDeclarativeResult = {
/** Search results */
results?: SearchResponse;
/** Loading state */
isLoading: boolean;
isFetching: boolean;
/** Error state */
error?: ApiErrorType;
/** Refetch with same params */
refetch: () => Promise;
/** Pagination (if link-based pagination) */
nextPage?: () => void;
previousPage?: () => void;
hasNextPage?: boolean;
hasPreviousPage?: boolean;
};
function useStacSearchDeclarative(
options: UseStacSearchDeclarativeOptions
): UseStacSearchDeclarativeResult;
```
### Implementation
```typescript
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
```typescript
function SimpleSearch() {
const [collections, setCollections] = useState(['landsat-8']);
const [bbox, setBbox] = useState();
const { results, isLoading } = useStacSearchDeclarative({
params: { collections, bbox, limit: 50 },
});
// Automatically re-searches when collections or bbox change
return (
{isLoading ? : }
);
}
```
#### 2. URL-Synced Search
```typescript
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 ;
}
```
#### 3. With Custom Search Link
```typescript
function CustomEndpointSearch({ searchLink, bbox }) {
const { results, isLoading } = useStacSearchDeclarative({
params: { bbox, limit: 100 },
searchLink, // Use specific search endpoint
});
return
}
```
#### 4. Conditional Search
```typescript
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:
```typescript
// 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:
```typescript
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
- #5 - Add Standalone Hooks (Context-Free Mode)
- #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
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.