Skip to content

Add Generic STAC Resource Fetching #35

@AliceR

Description

@AliceR

Problem

Currently, stac-react assumes you're working with a structured STAC API with known endpoints (/collections, /search, etc.). There's no way to fetch an arbitrary STAC resource (catalog, collection, or item) from any URL without knowing its type in advance.

This is problematic when:

  • Following STAC links where the target type is unknown
  • Exploring static STAC catalogs
  • Working with STAC resources from various sources
  • Building generic STAC browsers/explorers

Current Behavior

You must know the resource type and use the specific hook:

// Must know it's a collection
const { collection } = useCollection('collection-id');

// Must know it's an item  
const { item } = useItem(itemUrl);

// No way to fetch "whatever is at this URL"

Desired Behavior

import { useStacValue } from '@developmentseed/stac-react';

function StacResourceViewer({ url }) {
  const { value, type, isLoading, error } = useStacValue(url);
  
  // value could be Catalog, Collection, Item, or ItemCollection
  // type indicates what was fetched: 'Catalog' | 'Collection' | 'Feature' | 'FeatureCollection'
  
  switch (type) {
    case 'Catalog':
      return <CatalogView catalog={value} />;
    case 'Collection':
      return <CollectionView collection={value} />;
    case 'Feature':
      return <ItemView item={value} />;
    case 'FeatureCollection':
      return <ItemCollectionView items={value} />;
  }
}

Use Cases from stac-map

stac-map has a useStacValue hook that:

const { value, error, items, table } = useStacValue({
  href: 'https://any-stac-url.com/resource.json',
  fileUpload,
  datetimeBounds,
  stacGeoparquetItemId,
});

// value is automatically typed based on what was fetched
// Works with URLs, file uploads, and geoparquet

Proposed Solution

Hook Signature

type StacValueType = 'Catalog' | 'Collection' | 'Feature' | 'FeatureCollection';

type StacValue = StacCatalog | StacCollection | StacItem | StacItemCollection;

type UseStacValueResult = {
  value?: StacValue;
  type?: StacValueType;
  isLoading: boolean;
  isFetching: boolean;
  error?: ApiErrorType;
  refetch: () => Promise<void>;
};

function useStacValue(
  url: string,
  options?: {
    headers?: Record<string, string>;
    method?: 'GET' | 'POST';
    body?: unknown;
  }
): UseStacValueResult;

Implementation

import { useQuery } from '@tanstack/react-query';

function useStacValue(url: string, options = {}): UseStacValueResult {
  const { data, error, isLoading, isFetching, refetch } = useQuery({
    queryKey: ['stac-value', url, options],
    queryFn: async () => {
      const response = await fetch(url, {
        method: options.method || 'GET',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          ...options.headers,
        },
        body: options.body ? JSON.stringify(options.body) : undefined,
      });

      if (!response.ok) {
        throw new ApiError(
          response.statusText,
          response.status,
          await response.text(),
          response.url
        );
      }

      const json = await response.json();
      return {
        value: json,
        type: determineStacType(json),
      };
    },
    enabled: !!url,
    retry: false,
  });

  return {
    value: data?.value,
    type: data?.type,
    isLoading,
    isFetching,
    error,
    refetch,
  };
}

function determineStacType(json: any): StacValueType {
  if (json.type === 'Feature') return 'Feature';
  if (json.type === 'FeatureCollection') return 'FeatureCollection';
  if (json.type === 'Collection') return 'Collection';
  if (json.type === 'Catalog') return 'Catalog';
  
  // Fallback: try to infer from structure
  if (json.extent && json.license) return 'Collection';
  if (json.links && !json.features) return 'Catalog';
  
  throw new Error('Unable to determine STAC type from resource');
}

Advanced Features

Type Guards

export function isStacCatalog(value: StacValue): value is StacCatalog {
  return value.type === 'Catalog';
}

export function isStacCollection(value: StacValue): value is StacCollection {
  return value.type === 'Collection';
}

export function isStacItem(value: StacValue): value is StacItem {
  return value.type === 'Feature';
}

export function isStacItemCollection(value: StacValue): value is StacItemCollection {
  return value.type === 'FeatureCollection';
}

Link Resolution

Optionally resolve relative hrefs to absolute:

function makeHrefsAbsolute(value: StacValue, baseUrl: string): StacValue {
  if (!value.links) return value;
  
  return {
    ...value,
    links: value.links.map(link => ({
      ...link,
      href: new URL(link.href, baseUrl).toString(),
    })),
  };
}

Benefits

  • ✅ Generic STAC resource fetching
  • ✅ Type-safe handling of different STAC types
  • ✅ Simplifies link following in STAC catalogs
  • ✅ Enables building generic STAC explorers
  • ✅ Matches patterns used in other STAC tools

Breaking Changes

None - this is new functionality.

Related Issues

Testing Requirements

  • Test with STAC catalogs
  • Test with STAC collections
  • Test with STAC items
  • Test with item collections
  • Test type determination logic
  • Test error handling for invalid STAC
  • Test with relative and absolute hrefs

Documentation Requirements

  • Document the useStacValue hook
  • Provide examples for each STAC type
  • Document type guards and utilities
  • Show link following patterns

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