Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 47 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,75 @@ With Yarn:
yarn add @developmentseed/stac-react
```

### Peer Dependency: @tanstack/react-query

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**. This means you must install it in your project:

```sh
npm install @tanstack/react-query
# or
yarn add @tanstack/react-query
```

If you do not install it, your package manager will warn you, and stac-react will not work correctly.

## Getting started

Stac-react's hooks must be used inside children of a React context that provides access to the stac-react's core functionality.
stac-react's hooks must be used inside children of a React context that provides access to the stac-react's core functionality.

To get started, initialize `StacApiProvider` with the base URL of the STAC catalog.
To get started, initialize `StacApiProvider` with the base URL of the STAC catalog. `StacApiProvider` automatically sets up a [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) QueryClientProvider for you, so you do not need to wrap your app with QueryClientProvider yourself.

```jsx
import { StacApiProvider } from "stac-react";
import { StacApiProvider } from 'stac-react';

function StacApp() {
return (
<StacApiProvider apiUrl="https://my-stac-api.com">
// Other components
</StacApiProvide>
<StacApiProvider apiUrl="https://my-stac-api.com">{/* Other components */}</StacApiProvider>
);
}
```

If you want to provide your own custom QueryClient (for advanced caching or devtools), you can pass it as a prop:

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

const queryClient = new QueryClient();

function StacApp() {
return (
<StacApiProvider apiUrl="https://my-stac-api.com" queryClient={queryClient}>
{/* Other components */}
</StacApiProvider>
);
}
```

For additional information, see the React Query setup guide: [docs/react-query-setup.md](docs/react-query-setup.md).

Now you can start using stac-react hooks in child components of `StacApiProvider`

```jsx
import { StacApiProvider, useCollections } from "stac-react";
import { StacApiProvider, useCollections } from 'stac-react';

function Collections() {
const { collections } = useCollections();

return (
<ul>
{collections.collections.map(({ id, title }) => (
<li key={id}>{ title }</li>
))}
</ul>

)
<ul>
{collections.collections.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
);
}

function StacApp() {
return (
<StacApiProvider apiUrl="https://my-stac-api.com">
<Collections />
</StacApiProvide>
</StacApiProvider>
);
}
```
Expand All @@ -73,14 +101,10 @@ Provides the React context required for stac-react hooks.
#### Initialization

```jsx
import { StacApiProvider } from "stac-react";
import { StacApiProvider } from 'stac-react';

function StacApp() {
return (
<StacApiProvider apiUrl="https://my-stac-api.com">
// Other components
</StacApiProvide>
);
return <StacApiProvider apiUrl="https://my-stac-api.com">// Other components</StacApiProvider>;
}
```

Expand Down Expand Up @@ -471,9 +495,9 @@ function StacComponent() {
```

| Option | Type | Description |
| ------------ | -------- | --------------------------------- | ------------------------------------------------------------------------------------------- |
| ------------ | -------- | --------------------------------- | ------------------------------------------------------------------------------------------ |
| `detail` | `string` | `object | The error return from the API. Either a`string` or and `object` depending on the response. |
| `status` | `number` | HTTP status code of the response. |
| `status` | `number` | HTTP status code of the response. |
| `statusText` | `string` | Status text for the response. |

## Development
Expand Down
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
57 changes: 57 additions & 0 deletions example/src/pages/Main/ItemDetails.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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, state, error, reload } = useItem(itemUrl);

const isLoading = state === 'LOADING';

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;
Loading