8000 Add Flexible Search Hook · Issue #38 · developmentseed/stac-react · GitHub
[go: up one dir, main page]

Skip to content

Add Flexible Search Hook #38

@AliceR

Description

@AliceR

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} />;
}
8000

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

  • 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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0