Add Flexible Search Hook

Open
#38 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
30/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Stale
Tech stack
react, typescript
Domain
api, frontend

Research direction

Start by reading the existing hooks/useStacSearch implementation and its public export, then review related issues #5 and #10 before deciding how the new hook fits. Done means the declarative and stateful hooks coexist, the requested parameter, context, link, pagination, error, and enabled-state tests pass, and the usage and migration documentation is added.

Written by the indexing model from the issue text.

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 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

  • #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
Dominant language
TypeScript
Stars
34
Forks
4
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from developmentseed/stac-react

All issues in developmentseed/stac-react

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.