developmentseed / developmentseed/stac-react

Add Child Resource Navigation

Open
#37 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
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
```typescript
// 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
```typescript
import { useStacChildren } from '@developmentseed/stac-react';

function CatalogBrowser({ catalog }) {
const { catalogs, collections, isLoading, error } = useStacChildren(catalog);

return (


Child Catalogs


{catalogs?.map(cat => )}

Collections


{collections?.map(col => )}

);
}
```

## Use Cases from stac-map

stac-map has `useStacChildren` that:
```typescript
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
```typescript
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
```typescript
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
```typescript
function CatalogViewer({ catalog }) {
const { catalogs, collections, isLoading } = useStacChildren({
value: catalog
});

if (isLoading) return ;

return (


{catalogs && (

Subcatalogs ({catalogs.length})


{catalogs.map(cat => (

))}

)}

{collections && (

Collections ({collections.length})


{collections.map(col => (

))}

)}

);
}
```

#### 2. Recursive Catalog Tree
```typescript
function CatalogTree({ catalog, depth = 0 }) {
const { catalogs, collections } = useStacChildren({
value: catalog,
enabled: depth < 3, // Limit depth
});

return (


{catalog.title || catalog.id}



{collections?.map(col => (

))}

{catalogs?.map(cat => (

))}

);
}
```

#### 3. Collection Items
```typescript
function CollectionItemsGrid({ collection }) {
const { items, isLoading } = useStacChildren({
value: collection,
relations: ['item'], // Only fetch items
});

return (


{items?.map(item => (

))}

);
}
```

#### 4. Conditional Loading
```typescript
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
```typescript
// Follow any custom link relations
const { children } = useStacChildren({
value: catalog,
relations: ['child', 'item', 'derived_from'],
});
```

### Progress Tracking
```typescript
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

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.