Skip to content

Add Child Resource Navigation #37

@AliceR

Description

@AliceR

Problem

STAC catalogs and collections contain links to child resources (child catalogs, collections, or items). Currently, stac-react has no built-in way to:

  • Fetch all child resources from a parent
  • Navigate catalog hierarchies
  • Browse collections within a catalog
  • Fetch items linked from a collection

Users must manually parse links and create their own queries, duplicating logic across applications.

Current Behavior

// Must manually parse links and fetch
const { collection } = useCollection('my-collection');

if (collection?.links) {
  const childLinks = collection.links.filter(l => l.rel === 'child');
  // Now what? Manually fetch each one?
}

No built-in way to fetch child resources automatically.

Desired Behavior

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

function CatalogBrowser({ catalog }) {
  const { catalogs, collections, isLoading, error } = useStacChildren(catalog);
  
  return (
    <div>
      <h2>Child Catalogs</h2>
      {catalogs?.map(cat => <CatalogCard key={cat.id} catalog={cat} />)}
      
      <h2>Collections</h2>
      {collections?.map(col => <CollectionCard key={col.id} collection={col} />)}
    </div>
  );
}

Use Cases from stac-map

stac-map has useStacChildren that:

const { catalogs, collections } = useStacChildren({
  value, // Any STAC resource
  enabled: !!value && !collectionsLink,
});

// Automatically:
// 1. Finds all links with rel="child"
// 2. Fetches each child resource
// 3. Separates catalogs from collections
// 4. Returns typed arrays

Proposed Solution

Hook Signature

type UseStacChildrenOptions = {
  /** The parent STAC resource (catalog, collection, or item) */
  value?: StacCatalog | StacCollection | StacItem;
  
  /** Enable/disable the queries */
  enabled?: boolean;
  
  /** Which link relations to follow (default: ['child', 'item']) */
  relations?: string[];
};

type UseStacChildrenResult = {
  /** Child catalogs found */
  catalogs?: StacCatalog[];
  
  /** Child collections found */
  collections?: StacCollection[];
  
  /** Child items found */
  items?: StacItem[];
  
  /** All children (mixed types) */
  children?: (StacCatalog | StacCollection | StacItem)[];
  
  /** Loading state */
  isLoading: boolean;
  
  /** Error state */
  error?: Error;
};

function useStacChildren(
  options: UseStacChildrenOptions
): UseStacChildrenResult;

Implementation

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

function useStacChildren({
  value,
  enabled = true,
  relations = ['child', 'item'],
}: UseStacChildrenOptions): UseStacChildrenResult {
  
  // Find child links
  const childLinks = useMemo(() => {
    if (!value?.links) return [];
    return value.links.filter(link => relations.includes(link.rel));
  }, [value, relations]);

  // Fetch all children in parallel
  const results = useQueries({
    queries: childLinks.map(link => ({
      queryKey: ['stac-value', link.href],
      queryFn: async () => {
        const response = await fetch(link.href, {
          headers: { 'Accept': 'application/json' },
        });
        
        if (!response.ok) {
          throw new Error(`Failed to fetch ${link.href}: ${response.statusText}`);
        }
        
        return response.json();
      },
      enabled: enabled && !!value,
      retry: false,
    })),
    combine: (results) => ({
      data: results.map(r => r.data).filter(Boolean),
      isLoading: results.some(r => r.isLoading),
      error: results.find(r => r.error)?.error,
    }),
  });

  // Separate by type
  return useMemo(() => {
    const catalogs: StacCatalog[] = [];
    const collections: StacCollection[] = [];
    const items: StacItem[] = [];
    
    for (const child of results.data) {
      switch (child?.type) {
        case 'Catalog':
          catalogs.push(child);
          break;
        case 'Collection':
          collections.push(child);
          break;
        case 'Feature':
          items.push(child);
          break;
      }
    }
    
    return {
      catalogs: catalogs.length > 0 ? catalogs : undefined,
      collections: collections.length > 0 ? collections : undefined,
      items: items.length > 0 ? items : undefined,
      children: results.data,
      isLoading: results.isLoading,
      error: results.error,
    };
  }, [results.data, results.isLoading, results.error]);
}

Example Usage Patterns

1. Catalog Browser

function CatalogViewer({ catalog }) {
  const { catalogs, collections, isLoading } = useStacChildren({ 
    value: catalog 
  });
  
  if (isLoading) return <Loading />;
  
  return (
    <div>
      {catalogs && (
        <section>
          <h2>Subcatalogs ({catalogs.length})</h2>
          {catalogs.map(cat => (
            <CatalogCard key={cat.id} catalog={cat} />
          ))}
        </section>
      )}
      
      {collections && (
        <section>
          <h2>Collections ({collections.length})</h2>
          {collections.map(col => (
            <CollectionCard key={col.id} collection={col} />
          ))}
        </section>
      )}
    </div>
  );
}

2. Recursive Catalog Tree

function CatalogTree({ catalog, depth = 0 }) {
  const { catalogs, collections } = useStacChildren({ 
    value: catalog,
    enabled: depth < 3, // Limit depth
  });
  
  return (
    <div style={{ marginLeft: depth * 20 }}>
      <h3>{catalog.title || catalog.id}</h3>
      
      {collections?.map(col => (
        <CollectionItem key={col.id} collection={col} />
      ))}
      
      {catalogs?.map(cat => (
        <CatalogTree key={cat.id} catalog={cat} depth={depth + 1} />
      ))}
    </div>
  );
}

3. Collection Items

function CollectionItemsGrid({ collection }) {
  const { items, isLoading } = useStacChildren({ 
    value: collection,
    relations: ['item'], // Only fetch items
  });
  
  return (
    <div className="grid">
      {items?.map(item => (
        <ItemCard key={item.id} item={item} />
      ))}
    </div>
  );
}

4. Conditional Loading

function ConditionalChildren({ value, expanded }) {
  const { catalogs, collections } = useStacChildren({ 
    value,
    enabled: expanded, // Only fetch when expanded
  });
  
  // Children only load when user expands the section
}

Advanced Features

Custom Link Following

// Follow any custom link relations
const { children } = useStacChildren({
  value: catalog,
  relations: ['child', 'item', 'derived_from'],
});

Progress Tracking

function useStacChildrenWithProgress(value) {
  const childLinks = value?.links?.filter(l => l.rel === 'child') || [];
  const [completed, setCompleted] = useState(0);
  
  const results = useQueries({
    queries: childLinks.map(link => ({
      // ... query config
      onSuccess: () => setCompleted(c => c + 1),
    })),
  });
  
  return {
    ...results,
    progress: childLinks.length > 0 ? completed / childLinks.length : 0,
  };
}

Benefits

  • ✅ Automatic child resource fetching
  • ✅ Parallel loading for performance
  • ✅ Type separation (catalogs vs collections vs items)
  • ✅ Enables catalog tree navigation
  • ✅ Simplifies recursive browsing
  • ✅ Leverages React Query caching

Performance Considerations

  • Fetches all children in parallel (not sequential)
  • Respects React Query cache (won't refetch)
  • enabled prop allows lazy loading
  • Consider pagination for large child sets (future enhancement)

Breaking Changes

None - this is new functionality.

Testing Requirements

  • Test with catalogs containing children
  • Test with collections containing items
  • Test type separation logic
  • Test enabled/disabled state
  • Test error handling for failed child fetches
  • Test with empty/no children
  • Test custom relations
  • Test parallel loading performance

Documentation Requirements

  • Document the hook API
  • Provide catalog browser example
  • Show recursive tree pattern
  • Document performance characteristics
  • Show lazy loading patterns
  • Document custom link relations

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