Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a93b27c
docs: start architecture decision log and add ADR for fetch library d…
AliceR Oct 23, 2025
9bccaaa
feat: migrate useCollections hook to TanStack Query
AliceR Oct 23, 2025
e40fcce
feat: enable TanStack Query DevTools browser extension
AliceR Oct 27, 2025
a04443b
feat: migrate useItem hook to TanStack Query
AliceR Oct 27, 2025
3013303
docs(example): add item detail panel demonstrating useItem hook
AliceR Oct 27, 2025
9999717
feat: migrate useStacApi hook to TanStack Query
AliceR Oct 30, 2025
4d0f701
feat: migrate useStacSearch hook to TanStack Query
AliceR Nov 17, 2025
e0819c4
fix: externalize @tanstack/react-query in build and fix StacApiProvid…
AliceR Nov 17, 2025
1dfbf58
fix: generate index.d.ts at dist root for proper TypeScript imports
AliceR Nov 18, 2025
9f30e60
feat: optimize React Query cache keys
AliceR Nov 24, 2025
6f5713d
fix: make DevTools opt-in to avoid conflicts with consuming apps
AliceR Nov 25, 2025
bc3550d
docs: add comment explaining gcTime in test wrapper
AliceR Nov 25, 2025
0b4bcdc
refactor: replace Object.assign error pattern with ApiError class
AliceR Nov 27, 2025
3ddb6f2
refactor: replace queryClient prop with auto-detecting parent QueryCl…
AliceR Nov 27, 2025
363d53e
test: add StacApiProvider test coverage
AliceR Nov 27, 2025
fd431a3
refactor!: remove collections/items state from context
AliceR Nov 27, 2025
802d1a2
Revert "refactor: replace queryClient prop with auto-detecting parent…
AliceR Nov 27, 2025
dc9c226
test: update tests to use queryClient prop when providing custom client
AliceR Nov 27, 2025
2752ceb
example: add custom QueryClient with optimized caching
AliceR Nov 27, 2025
f86970f
docs: update QueryClient example and update API reference
AliceR Nov 27, 2025
3236166
refactor!: replace LoadingState with React Query's isLoading/isFetching
AliceR Nov 27, 2025
c785dea
refactor: fetch single collection directly from /collections/{id} end…
AliceR Nov 27, 2025
0109e59
fix: properly type reload as async across all hooks
AliceR Nov 27, 2025
44882cb
refactor!: rename reload to refetch in hooks API
AliceR Nov 27, 2025
9ecb267
docs: add migration guide for v1.0.0 (TanStack Query integration)
AliceR Nov 28, 2025
cc60b1d
chore: bump version to 1.0.0
AliceR Nov 28, 2025
6b75fcc
fix: make date range parameters optional in useStacSearch hook
AliceR Nov 28, 2025
ec62ab4
fix: add comprehensive error handling for JSON parsing failures
AliceR Nov 28, 2025
9cb92fa
fix: prevent memory leak in debounced submit function
AliceR Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 99 additions & 55 deletions README.md

Large diffs are not rendered by default.

685 changes: 685 additions & 0 deletions docs/MIGRATION.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions docs/adr/0000-use-markdown-architectural-decision-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Use Markdown Architectural Decision Records

## Context and Problem Statement

We want to record architectural decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields.
Which format and structure should these records follow?

## Considered Options

- [MADR](https://adr.github.io/madr/) 4.0.0 – The Markdown Architectural Decision Records
- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR"
- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements
- Other templates listed at <https://github.com/joelparkerhenderson/architecture_decision_record>
- Formless – No conventions for file format and structure

## Decision Outcome

Chosen option: "MADR 4.0.0", because

- Implicit assumptions should be made explicit.
Design documentation is important to enable people understanding the decisions later on.
See also ["A rational design process: How and why to fake it"](https://doi.org/10.1109/TSE.1986.6312940).
- MADR allows for structured capturing of any decision.
- The MADR format is lean and fits our development style.
- The MADR structure is comprehensible and facilitates usage & maintenance.
- The MADR project is vivid.
66 changes: 66 additions & 0 deletions docs/adr/0001-use-a-fetch-library-for-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
# These are optional metadata elements. Feel free to remove any of them.
status: "accepted"
date: 2025-09-18
decision-makers: @gadomski @AliceR
---

# Use a fetch library for caching

## Context and Problem Statement

Currently, `stac-react` uses the native `fetch` API for all STAC requests, with no built-in caching or request deduplication. As the library is intended for use in applications that may navigate between many STAC resources, efficient caching and request management are important for performance and developer experience.

## Decision Drivers

- Improve performance by caching repeated requests.
- Reduce network usage and latency.
- Provide a more robust API for request state, error handling, and background updates.
- Align with common React ecosystem practices.

## Considered Options

- Continue using native `fetch` with custom caching logic.
- Use TanStack Query (`@tanstack/react-query`) for fetching and caching.
- Use another fetch/caching library (e.g., SWR, Axios with custom cache).

## Decision Outcome

**Chosen option:** Use TanStack Query (`@tanstack/react-query`).

**Justification:**
TanStack Query is widely adopted, well-documented, and provides robust caching, request deduplication, background refetching, and React integration. It will make `stac-react` more attractive to downstream applications and reduce the need for custom caching logic.

### Consequences

- **Good:** Improved performance and developer experience; less custom code for caching and request state.
- **Bad:** Adds a new dependency and requires refactoring existing hooks to use TanStack Query.

### Confirmation

- Implementation will be confirmed by refactoring hooks to use TanStack Query and verifying caching behavior in tests and example app.
- Code review will ensure correct usage and integration.

## Pros and Cons of the Options

### TanStack Query

- **Good:** Robust caching, request deduplication, background updates, React integration.
- **Good:** Well-supported and documented.
- **Neutral:** Adds a dependency, but it is widely used.
- **Bad:** Requires refactoring and learning curve for maintainers.

### Native Fetch

- **Good:** No new dependencies.
- **Bad:** No built-in caching, more custom code required, less robust for complex scenarios.

### Other Libraries (SWR, Axios)

- **Good:** Some provide caching, but less feature-rich or less adopted for React.
- **Bad:** May require more custom integration.

## More Information

- [TanStack Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview)
- This ADR will be revisited if TanStack Query no longer meets project needs or if a better alternative emerges.
52 changes: 52 additions & 0 deletions docs/react-query-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# QueryClient Best Practice

stac-react relies on [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) for data fetching and caching. To avoid duplicate React Query clients and potential version conflicts, stac-react lists `@tanstack/react-query` as a **peer dependency**.

## Why peer dependency?

- Prevents multiple versions of React Query in your app.
- Ensures your app and stac-react share the same QueryClient instance.
- Follows best practices for React libraries that integrate with popular frameworks.

stac-react manages the QueryClient for you by default, but you can provide your own for advanced use cases.

**Important:** If your app uses multiple providers that require a TanStack QueryClient (such as `QueryClientProvider` and `StacApiProvider`), always use the same single QueryClient instance for all providers. This ensures that queries, mutations, and cache are shared across your app and prevents cache fragmentation or duplicate network requests.

**Example:**

```jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StacApiProvider } from 'stac-react';

const queryClient = new QueryClient();

function App() {
return (
<QueryClientProvider client={queryClient}>
<StacApiProvider apiUrl="https://my-stac-api.com" queryClient={queryClient}>
{/* ...your app... */}
</StacApiProvider>
</QueryClientProvider>
);
}
```

If you do not pass the same QueryClient instance, each provider will maintain its own cache, which can lead to unexpected behavior.

## TanStack Query DevTools Integration

stac-react automatically connects your QueryClient to the [TanStack Query DevTools browser extension](https://tanstack.com/query/latest/docs/framework/react/devtools) when running in development mode. This allows you to inspect queries, mutations, and cache directly in your browser without adding extra dependencies to your project.

**How it works:**

- In development (`process.env.NODE_ENV === 'development'`), stac-react exposes the QueryClient on `window.__TANSTACK_QUERY_CLIENT__`.
- The browser extension detects this and connects automatically.
- No code changes or additional dependencies are required.

> By default, React Query Devtools are only included in bundles when process.env.NODE_ENV === 'development', so you don't need to worry about excluding them during a production build.

**Alternative:**

- If you prefer an embedded/floating devtools panel, you can install and use the [TanStack Query Devtools React component](https://tanstack.com/query/latest/docs/framework/react/devtools#floating-devtools) in your app. This adds a UI panel directly to your app, but increases bundle size and dependencies.

For more details, see the [TanStack Query DevTools documentation](https://tanstack.com/query/latest/docs/framework/react/devtools).
10 changes: 5 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ export default defineConfig([
],
// TODO: Consider making these errors in the future (use recommendedTypeChecked rules!).
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
},
},
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@mapbox/mapbox-gl-draw": "^1.3.0",
"@mapbox/mapbox-gl-draw-static-mode": "^1.0.1",
"@tanstack/react-query": "^5.90.10",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
Expand Down
44 changes: 36 additions & 8 deletions example/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,46 @@
import { StacApiProvider } from 'stac-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Header from './layout/Header';
import Main from './pages/Main';

// Create a QueryClient with custom cache configuration
// IMPORTANT: Must be created outside the component to maintain cache across renders
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// STAC data doesn't change frequently, so we can cache it for 5 minutes
staleTime: 5 * 60 * 1000, // 5 minutes
// Keep unused data in cache for 10 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
retry: 1,
// Disable automatic refetching since STAC data is static
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
},
},
});

function App() {
const apiUrl = process.env.REACT_APP_STAC_API;
const isDevelopment = process.env.NODE_ENV === 'development';

// Debug: Verify QueryClient configuration
if (isDevelopment && typeof window !== 'undefined') {
console.log('[App] QueryClient defaults:', queryClient.getDefaultOptions());
}

return (
<StacApiProvider apiUrl={apiUrl}>
<div className="App grid grid-rows-[min-content_1fr]">
<Header />
<main className="flex items-stretch">
<Main />
</main>
</div>
</StacApiProvider>
<QueryClientProvider client={queryClient}>
<StacApiProvider apiUrl={apiUrl} enableDevTools={isDevelopment} queryClient={queryClient}>
<div className="App grid grid-rows-[min-content_1fr]">
<Header />
<main className="flex items-stretch">
<Main />
</main>
</div>
</StacApiProvider>
</QueryClientProvider>
);
}

Expand Down
55 changes: 55 additions & 0 deletions example/src/pages/Main/ItemDetails.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useItem } from 'stac-react';

import { H2 } from '../../components/headers';
import Panel from '../../layout/Panel';
import { Button } from '../../components/buttons';

function ItemDetails({ item, onClose }) {
const itemUrl = item.links.find((r) => r.rel === 'self')?.href;
const { item: newItem, isLoading, error, reload } = useItem(itemUrl);

return (
<Panel className="grid grid-rows-[1fr_min-content] p-4 h-[calc(100vh_-_90px)] overflow-y-scroll w-full overflow-hidden">
<div className="w-full overflow-hidden">
<div className="flex flex-wrap items-start gap-2">
<H2 className="whitespace-normal break-words flex-1">Selected Item</H2>
<Button
type="button"
onClick={onClose}
aria-label="Close selected item panel"
title="Close"
className="p-2 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-300"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</Button>
</div>
{isLoading && <p className="whitespace-normal break-words">Loading...</p>}
{error && <p className="whitespace-normal break-words">{error}</p>}
{newItem && (
<pre className="bg-gray-100 p-2 rounded w-full whitespace-pre-wrap break-words overflow-x-auto text-xs">
{JSON.stringify(newItem, null, 2)}
</pre>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<Button type="button" onClick={reload}>
Reload
</Button>
</div>
</Panel>
);
}
export default ItemDetails;
13 changes: 9 additions & 4 deletions example/src/pages/Main/ItemList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ PaginationButton.propTypes = {
children: T.node.isRequired,
};

function ItemList({ items, isLoading, error, nextPage, previousPage }) {
function ItemList({ items, isLoading, error, nextPage, previousPage, onSelect }) {
return (
<Panel className="grid grid-rows-[1fr_min-content] p-4">
<div className="overflow-x-clip">
<H2>Item List</H2>
{isLoading && <p>Loading...</p>}
{error && <p>{error}</p>}
{items && (
<ul>
{items.features.map(({ id }) => (
<li key={id}>{id}</li>
<ul className="space-y-2">
{items.features.map((item) => (
<li key={item.id}>
<button onClick={onSelect(item)} className="text-pretty">
{item.id}
</button>
</li>
))}
</ul>
)}
Expand All @@ -52,6 +56,7 @@ ItemList.propTypes = {
error: T.string,
previousPage: T.func,
nextPage: T.func,
onSelect: T.func,
};

export default ItemList;
34 changes: 26 additions & 8 deletions example/src/pages/Main/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useStacSearch, useCollections, useStacApi, StacApiProvider } from 'stac
import ItemList from './ItemList';
import Map from './Map';
import QueryBuilder from './QueryBuilder';
import ItemDetails from './ItemDetails';

// eslint-disable-next-line no-unused-vars
const options = {
Expand All @@ -26,7 +27,7 @@ function Main() {
setDateRangeTo,
submit,
results,
state,
isLoading,
error,
nextPage,
previousPage,
Expand All @@ -43,6 +44,18 @@ function Main() {
[setBbox]
);

const [selectedItem, setSelectedItem] = useState(null);

const onSelect = useCallback(
(item) => () => {
setSelectedItem(item);
},
[]
);
const onClose = useCallback(() => {
setSelectedItem(null);
}, []);

return (
<div className="grid grid-cols-4 gap-4 m-4">
<QueryBuilder
Expand All @@ -56,13 +69,18 @@ function Main() {
dateRangeTo={dateRangeTo}
setDateRangeTo={setDateRangeTo}
/>
<ItemList
items={results}
isLoading={state === 'LOADING'}
error={error && 'Error loading results'}
nextPage={nextPage}
previousPage={previousPage}
/>
{selectedItem ? (
<ItemDetails item={selectedItem} onClose={onClose} />
) : (
<ItemList
items={results}
isLoading={isLoading}
error={error && 'Error loading results'}
nextPage={nextPage}
previousPage={previousPage}
onSelect={onSelect}
/>
)}
<Map
className="col-span-2"
isBboxDrawEnabled={isBboxDrawEnabled}
Expand Down
Loading