developmentseed / developmentseed/stac-react

Add Flexible Search Hook

Open
#38 0 comments 0 reactions 0 assignees View on GitHub
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.