Skip to content

Commit 3b6f2b2

Browse files
committed
Refactors STAC API context and hooks
Allows headers to be set globally for STAC API Moves STAC API logic into a context provider, allowing for easier management of API state and access to STAC API instance, collections, and item data. It also introduces pagination to the collections hook.
1 parent a619465 commit 3b6f2b2

File tree

6 files changed

+258
-138
lines changed

6 files changed

+258
-138
lines changed

src/context/context.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createContext } from 'react';
2+
import StacApi from '../stac-api';
3+
import { CollectionsResponse, Item } from '../types/stac';
4+
5+
type StacApiContextType = {
6+
stacApi?: StacApi;
7+
collections?: CollectionsResponse;
8+
setCollections: (collections?: CollectionsResponse) => void;
9+
getItem: (id: string) => Item | undefined;
10+
addItem: (id: string, item: Item) => void;
11+
deleteItem: (id: string) => void;
12+
};
13+
14+
export const StacApiContext = createContext<StacApiContextType>(
15+
{} as StacApiContextType
16+
);

src/context/index.tsx

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,65 @@
11
import React, { useMemo, useContext, useState, useCallback } from 'react';
2-
import { createContext } from 'react';
32

4-
import StacApi from '../stac-api';
53
import useStacApi from '../hooks/useStacApi';
64
import type { CollectionsResponse, Item } from '../types/stac';
75
import { GenericObject } from '../types';
8-
9-
type StacApiContextType = {
10-
stacApi?: StacApi;
11-
collections?: CollectionsResponse;
12-
setCollections: (collections?: CollectionsResponse) => void;
13-
getItem: (id: string) => Item | undefined;
14-
addItem: (id: string, item: Item) => void;
15-
deleteItem: (id: string) => void;
16-
}
6+
import { StacApiContext } from './context';
177

188
type StacApiProviderType = {
199
apiUrl: string;
2010
children: React.ReactNode;
2111
options?: GenericObject;
22-
}
12+
};
2313

24-
export const StacApiContext = createContext<StacApiContextType>({} as StacApiContextType);
25-
26-
export function StacApiProvider({ children, apiUrl, options }: StacApiProviderType) {
14+
export function StacApiProvider({
15+
children,
16+
apiUrl,
17+
options
18+
}: StacApiProviderType) {
2719
const { stacApi } = useStacApi(apiUrl, options);
28-
const [ collections, setCollections ] = useState<CollectionsResponse>();
29-
const [ items, setItems ] = useState(new Map<string, Item>());
20+
const [collections, setCollections] = useState<CollectionsResponse>();
21+
const [items, setItems] = useState(new Map<string, Item>());
3022

3123
const getItem = useCallback((id: string) => items.get(id), [items]);
3224

33-
const addItem = useCallback((itemPath: string, item: Item) => {
34-
setItems(new Map(items.set(itemPath, item)));
35-
}, [items]);
25+
const addItem = useCallback(
26+
(itemPath: string, item: Item) => {
27+
setItems(new Map(items.set(itemPath, item)));
28+
},
29+
[items]
30+
);
3631

37-
const deleteItem = useCallback((itemPath: string) => {
38-
const tempItems = new Map(items);
39-
items.delete(itemPath);
40-
setItems(tempItems);
41-
}, [items]);
32+
const deleteItem = useCallback(
33+
(itemPath: string) => {
34+
const tempItems = new Map(items);
35+
items.delete(itemPath);
36+
setItems(tempItems);
37+
},
38+
[items]
39+
);
4240

43-
const contextValue = useMemo(() => ({
44-
stacApi,
45-
collections,
46-
setCollections,
47-
getItem,
48-
addItem,
49-
deleteItem
50-
}), [addItem, collections, deleteItem, getItem, stacApi]);
41+
const contextValue = useMemo(
42+
() => ({
43+
stacApi,
44+
collections,
45+
setCollections,
46+
getItem,
47+
addItem,
48+
deleteItem
49+
}),
50+
[addItem, collections, deleteItem, getItem, stacApi]
51+
);
5152

5253
return (
5354
<StacApiContext.Provider value={contextValue}>
54-
{ children }
55+
{children}
5556
</StacApiContext.Provider>
5657
);
5758
}
5859

5960
export function useStacApiContext() {
60-
const {
61-
stacApi,
62-
collections,
63-
setCollections,
64-
getItem,
65-
addItem,
66-
deleteItem
67-
} = useContext(StacApiContext);
61+
const { stacApi, collections, setCollections, getItem, addItem, deleteItem } =
62+
useContext(StacApiContext);
6863

6964
return {
7065
stacApi,

src/hooks/useCollection.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,70 @@
1-
import { useMemo, useState, useEffect } from 'react';
1+
import { useState, useEffect, useCallback } from 'react';
22

33
import type { ApiError, LoadingState } from '../types';
44
import type { Collection } from '../types/stac';
5-
import useCollections from './useCollections';
5+
import { useStacApiContext } from '../context';
66

77
type StacCollectionHook = {
8-
collection?: Collection,
9-
state: LoadingState,
10-
error?: ApiError,
11-
reload: () => void
8+
collection?: Collection;
9+
state: LoadingState;
10+
error?: ApiError;
11+
reload: () => void;
1212
};
1313

1414
function useCollection(collectionId: string): StacCollectionHook {
15-
const { collections, state, error: requestError, reload } = useCollections();
16-
const [ error, setError ] = useState<ApiError>();
15+
if (!collectionId) {
16+
throw new Error('Collection ID is required');
17+
}
1718

18-
useEffect(() => {
19-
setError(requestError);
20-
}, [requestError]);
21-
22-
const collection = useMemo(
23-
() => {
24-
const coll = collections?.collections.find(({ id }) => id === collectionId);
25-
if (!coll) {
26-
setError({
27-
status: 404,
28-
statusText: 'Not found',
29-
detail: 'Collection does not exist'
30-
});
19+
const { stacApi, collections } = useStacApiContext();
20+
21+
const [collection, setCollection] = useState<Collection>();
22+
const [state, setState] = useState<LoadingState>('IDLE');
23+
const [error, setError] = useState<ApiError>();
24+
25+
const load = useCallback(
26+
(id: string) => {
27+
if (stacApi) {
28+
setError(undefined);
29+
setState('LOADING');
30+
stacApi
31+
.getCollection(id)
32+
.then(async (res) => {
33+
const data: Collection = await res.json();
34+
setCollection(data);
35+
})
36+
.catch((err: ApiError) => {
37+
setError(err);
38+
})
39+
.finally(() => {
40+
setState('IDLE');
41+
});
3142
}
32-
return coll;
3343
},
34-
[collectionId, collections]
44+
[stacApi]
3545
);
3646

47+
useEffect(() => {
48+
setState('LOADING');
49+
// Check if the collection is already in the collections list.
50+
const coll = collections?.collections.find(({ id }) => id === collectionId);
51+
if (coll) {
52+
setCollection(coll);
53+
setState('IDLE');
54+
return;
55+
}
56+
57+
// If not, request the collection directly from the API.
58+
load(collectionId);
59+
}, [collectionId, collections, load]);
60+
3761
return {
3862
collection,
3963
state,
4064
error,
41-
reload
65+
reload: useCallback(() => {
66+
load(collectionId);
67+
}, [collectionId, load])
4268
};
4369
}
4470

src/hooks/useCollections.ts

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,90 @@
1-
import { useCallback, useEffect, useState, useMemo } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import { type ApiError, type LoadingState } from '../types';
33
import type { CollectionsResponse } from '../types/stac';
44
import debounce from '../utils/debounce';
55
import { useStacApiContext } from '../context';
66

77
type StacCollectionsHook = {
8-
collections?: CollectionsResponse,
9-
reload: () => void,
10-
state: LoadingState
11-
error?: ApiError
8+
collections?: CollectionsResponse;
9+
reload: () => void;
10+
state: LoadingState;
11+
error?: ApiError;
12+
nextPage?: () => void;
13+
prevPage?: () => void;
14+
setOffset: (newOffset: number) => void;
1215
};
1316

14-
function useCollections(): StacCollectionsHook {
17+
export default function useCollections(opts?: {
18+
limit?: number;
19+
initialOffset?: number;
20+
}): StacCollectionsHook {
21+
const { limit = 10, initialOffset = 0 } = opts || {};
22+
1523
const { stacApi, collections, setCollections } = useStacApiContext();
16-
const [ state, setState ] = useState<LoadingState>('IDLE');
17-
const [ error, setError ] = useState<ApiError>();
24+
const [state, setState] = useState<LoadingState>('IDLE');
25+
const [error, setError] = useState<ApiError>();
26+
27+
const [offset, setOffset] = useState(initialOffset);
28+
29+
const [hasNext, setHasNext] = useState(false);
30+
const [hasPrev, setHasPrev] = useState(false);
1831

1932
const _getCollections = useCallback(
20-
() => {
33+
async (offset: number, limit: number) => {
2134
if (stacApi) {
2235
setState('LOADING');
2336

24-
stacApi.getCollections()
25-
.then(response => response.json())
26-
.then(setCollections)
27-
.catch((err) => {
28-
setError(err);
29-
setCollections(undefined);
30-
})
31-
.finally(() => setState('IDLE'));
37+
try {
38+
const res = await stacApi.getCollections({ limit, offset });
39+
const data: CollectionsResponse = await res.json();
40+
41+
setHasNext(!!data.links?.find((l) => l.rel === 'next'));
42+
setHasPrev(
43+
!!data.links?.find((l) => ['prev', 'previous'].includes(l.rel))
44+
);
45+
46+
setCollections(data);
47+
} catch (err: any) {
48+
setError(err);
49+
setCollections(undefined);
50+
} finally {
51+
setState('IDLE');
52+
}
3253
}
3354
},
3455
[setCollections, stacApi]
3556
);
36-
const getCollections = useMemo(() => debounce(_getCollections), [_getCollections]);
3757

38-
useEffect(
39-
() => {
40-
if (stacApi && !error && !collections) {
41-
getCollections();
42-
}
43-
},
44-
[getCollections, stacApi, collections, error]
58+
const getCollections = useCallback(
59+
(offset: number, limit: number) =>
60+
debounce(() => _getCollections(offset, limit))(),
61+
[_getCollections]
4562
);
4663

64+
const nextPage = useCallback(() => {
65+
setOffset(offset + limit);
66+
}, [offset, limit]);
67+
68+
const prevPage = useCallback(() => {
69+
setOffset(offset - limit);
70+
}, [offset, limit]);
71+
72+
useEffect(() => {
73+
if (stacApi && !error && !collections) {
74+
getCollections(offset, limit);
75+
}
76+
}, [getCollections, stacApi, collections, error, offset, limit]);
77+
4778
return {
4879
collections,
49-
reload: getCollections,
80+
reload: useCallback(
81+
() => getCollections(offset, limit),
82+
[getCollections, offset, limit]
83+
),
84+
nextPage: hasNext ? nextPage : undefined,
85+
prevPage: hasPrev ? prevPage : undefined,
86+
setOffset,
5087
state,
51-
error,
88+
error
5289
};
5390
}
54-
55-
export default useCollections;

0 commit comments

Comments
 (0)