developmentseed / developmentseed/stac-react
Add Child Resource Navigation
- 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.