Skip to content

Commit a4b4881

Browse files
authored
Merge pull request #27 – Migrate to TanStack Query for Data Fetching and Caching
2 parents 4ad048d + 6849141 commit a4b4881

40 files changed

+2733
-769
lines changed

README.md

Lines changed: 99 additions & 55 deletions
Large diffs are not rendered by default.

docs/MIGRATION.md

Lines changed: 685 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Use Markdown Architectural Decision Records
2+
3+
## Context and Problem Statement
4+
5+
We want to record architectural decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields.
6+
Which format and structure should these records follow?
7+
8+
## Considered Options
9+
10+
- [MADR](https://adr.github.io/madr/) 4.0.0 – The Markdown Architectural Decision Records
11+
- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR"
12+
- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements
13+
- Other templates listed at <https://github.com/joelparkerhenderson/architecture_decision_record>
14+
- Formless – No conventions for file format and structure
15+
16+
## Decision Outcome
17+
18+
Chosen option: "MADR 4.0.0", because
19+
20+
- Implicit assumptions should be made explicit.
21+
Design documentation is important to enable people understanding the decisions later on.
22+
See also ["A rational design process: How and why to fake it"](https://doi.org/10.1109/TSE.1986.6312940).
23+
- MADR allows for structured capturing of any decision.
24+
- The MADR format is lean and fits our development style.
25+
- The MADR structure is comprehensible and facilitates usage & maintenance.
26+
- The MADR project is vivid.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
# These are optional metadata elements. Feel free to remove any of them.
3+
status: "accepted"
4+
date: 2025-09-18
5+
decision-makers: @gadomski @AliceR
6+
---
7+
8+
# Use a fetch library for caching
9+
10+
## Context and Problem Statement
11+
12+
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.
13+
14+
## Decision Drivers
15+
16+
- Improve performance by caching repeated requests.
17+
- Reduce network usage and latency.
18+
- Provide a more robust API for request state, error handling, and background updates.
19+
- Align with common React ecosystem practices.
20+
21+
## Considered Options
22+
23+
- Continue using native `fetch` with custom caching logic.
24+
- Use TanStack Query (`@tanstack/react-query`) for fetching and caching.
25+
- Use another fetch/caching library (e.g., SWR, Axios with custom cache).
26+
27+
## Decision Outcome
28+
29+
**Chosen option:** Use TanStack Query (`@tanstack/react-query`).
30+
31+
**Justification:**
32+
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.
33+
34+
### Consequences
35+
36+
- **Good:** Improved performance and developer experience; less custom code for caching and request state.
37+
- **Bad:** Adds a new dependency and requires refactoring existing hooks to use TanStack Query.
38+
39+
### Confirmation
40+
41+
- Implementation will be confirmed by refactoring hooks to use TanStack Query and verifying caching behavior in tests and example app.
42+
- Code review will ensure correct usage and integration.
43+
44+
## Pros and Cons of the Options
45+
46+
### TanStack Query
47+
48+
- **Good:** Robust caching, request deduplication, background updates, React integration.
49+
- **Good:** Well-supported and documented.
50+
- **Neutral:** Adds a dependency, but it is widely used.
51+
- **Bad:** Requires refactoring and learning curve for maintainers.
52+
53+
### Native Fetch
54+
55+
- **Good:** No new dependencies.
56+
- **Bad:** No built-in caching, more custom code required, less robust for complex scenarios.
57+
58+
### Other Libraries (SWR, Axios)
59+
60+
- **Good:** Some provide caching, but less feature-rich or less adopted for React.
61+
- **Bad:** May require more custom integration.
62+
63+
## More Information
64+
65+
- [TanStack Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview)
66+
- This ADR will be revisited if TanStack Query no longer meets project needs or if a better alternative emerges.

docs/react-query-setup.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# QueryClient Best Practice
2+
3+
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**.
4+
5+
## Why peer dependency?
6+
7+
- Prevents multiple versions of React Query in your app.
8+
- Ensures your app and stac-react share the same QueryClient instance.
9+
- Follows best practices for React libraries that integrate with popular frameworks.
10+
11+
stac-react manages the QueryClient for you by default, but you can provide your own for advanced use cases.
12+
13+
**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.
14+
15+
**Example:**
16+
17+
```jsx
18+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
19+
import { StacApiProvider } from 'stac-react';
20+
21+
const queryClient = new QueryClient();
22+
23+
function App() {
24+
return (
25+
<QueryClientProvider client={queryClient}>
26+
<StacApiProvider apiUrl="https://my-stac-api.com" queryClient={queryClient}>
27+
{/* ...your app... */}
28+
</StacApiProvider>
29+
</QueryClientProvider>
30+
);
31+
}
32+
```
33+
34+
If you do not pass the same QueryClient instance, each provider will maintain its own cache, which can lead to unexpected behavior.
35+
36+
## TanStack Query DevTools Integration
37+
38+
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.
39+
40+
**How it works:**
41+
42+
- In development (`process.env.NODE_ENV === 'development'`), stac-react exposes the QueryClient on `window.__TANSTACK_QUERY_CLIENT__`.
43+
- The browser extension detects this and connects automatically.
44+
- No code changes or additional dependencies are required.
45+
46+
> 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.
47+
48+
**Alternative:**
49+
50+
- 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.
51+
52+
For more details, see the [TanStack Query DevTools documentation](https://tanstack.com/query/latest/docs/framework/react/devtools).

eslint.config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ export default defineConfig([
9191
],
9292
// TODO: Consider making these errors in the future (use recommendedTypeChecked rules!).
9393
'@typescript-eslint/no-explicit-any': 'warn',
94-
'@typescript-eslint/no-unsafe-assignment': 'warn',
95-
'@typescript-eslint/no-unsafe-call': 'warn',
96-
'@typescript-eslint/no-unsafe-member-access': 'warn',
97-
'@typescript-eslint/no-unsafe-return': 'warn',
98-
'@typescript-eslint/no-unsafe-argument': 'warn',
94+
'@typescript-eslint/no-unsafe-assignment': 'off',
95+
'@typescript-eslint/no-unsafe-call': 'off',
96+
'@typescript-eslint/no-unsafe-member-access': 'off',
97+
'@typescript-eslint/no-unsafe-return': 'off',
98+
'@typescript-eslint/no-unsafe-argument': 'off',
9999
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
100100
},
101101
},

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"dependencies": {
66
"@mapbox/mapbox-gl-draw": "^1.3.0",
77
"@mapbox/mapbox-gl-draw-static-mode": "^1.0.1",
8+
"@tanstack/react-query": "^5.90.10",
89
"@testing-library/jest-dom": "^5.14.1",
910
"@testing-library/react": "^13.0.0",
1011
"@testing-library/user-event": "^13.2.1",

example/src/App.jsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
11
import { StacApiProvider } from 'stac-react';
2+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
23
import Header from './layout/Header';
34
import Main from './pages/Main';
45

6+
// Create a QueryClient with custom cache configuration
7+
// IMPORTANT: Must be created outside the component to maintain cache across renders
8+
const queryClient = new QueryClient({
9+
defaultOptions: {
10+
queries: {
11+
// STAC data doesn't change frequently, so we can cache it for 5 minutes
12+
staleTime: 5 * 60 * 1000, // 5 minutes
13+
// Keep unused data in cache for 10 minutes
14+
gcTime: 10 * 60 * 1000, // 10 minutes
15+
retry: 1,
16+
// Disable automatic refetching since STAC data is static
17+
refetchOnWindowFocus: false,
18+
refetchOnMount: false,
19+
refetchOnReconnect: false,
20+
},
21+
},
22+
});
23+
524
function App() {
625
const apiUrl = process.env.REACT_APP_STAC_API;
26+
const isDevelopment = process.env.NODE_ENV === 'development';
27+
28+
// Debug: Verify QueryClient configuration
29+
if (isDevelopment && typeof window !== 'undefined') {
30+
console.log('[App] QueryClient defaults:', queryClient.getDefaultOptions());
31+
}
32+
733
return (
8-
<StacApiProvider apiUrl={apiUrl}>
9-
<div className="App grid grid-rows-[min-content_1fr]">
10-
<Header />
11-
<main className="flex items-stretch">
12-
<Main />
13-
</main>
14-
</div>
15-
</StacApiProvider>
34+
<QueryClientProvider client={queryClient}>
35+
<StacApiProvider apiUrl={apiUrl} enableDevTools={isDevelopment} queryClient={queryClient}>
36+
<div className="App grid grid-rows-[min-content_1fr]">
37+
<Header />
38+
<main className="flex items-stretch">
39+
<Main />
40+
</main>
41+
</div>
42+
</StacApiProvider>
43+
</QueryClientProvider>
1644
);
1745
}
1846

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useItem } from 'stac-react';
2+
3+
import { H2 } from '../../components/headers';
4+
import Panel from '../../layout/Panel';
5+
import { Button } from '../../components/buttons';
6+
7+
function ItemDetails({ item, onClose }) {
8+
const itemUrl = item.links.find((r) => r.rel === 'self')?.href;
9+
const { item: newItem, isLoading, error, reload } = useItem(itemUrl);
10+
11+
return (
12+
<Panel className="grid grid-rows-[1fr_min-content] p-4 h-[calc(100vh_-_90px)] overflow-y-scroll w-full overflow-hidden">
13+
<div className="w-full overflow-hidden">
14+
<div className="flex flex-wrap items-start gap-2">
15+
<H2 className="whitespace-normal break-words flex-1">Selected Item</H2>
16+
<Button
17+
type="button"
18+
onClick={onClose}
19+
aria-label="Close selected item panel"
20+
title="Close"
21+
className="p-2 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-300"
22+
>
23+
<svg
24+
xmlns="http://www.w3.org/2000/svg"
25+
className="w-5 h-5"
26+
viewBox="0 0 24 24"
27+
fill="none"
28+
stroke="currentColor"
29+
>
30+
<path
31+
strokeWidth="2"
32+
strokeLinecap="round"
33+
strokeLinejoin="round"
34+
d="M6 18L18 6M6 6l12 12"
35+
/>
36+
</svg>
37+
</Button>
38+
</div>
39+
{isLoading && <p className="whitespace-normal break-words">Loading...</p>}
40+
{error && <p className="whitespace-normal break-words">{error}</p>}
41+
{newItem && (
42+
<pre className="bg-gray-100 p-2 rounded w-full whitespace-pre-wrap break-words overflow-x-auto text-xs">
43+
{JSON.stringify(newItem, null, 2)}
44+
</pre>
45+
)}
46+
</div>
47+
<div className="grid grid-cols-2 gap-4">
48+
<Button type="button" onClick={reload}>
49+
Reload
50+
</Button>
51+
</div>
52+
</Panel>
53+
);
54+
}
55+
export default ItemDetails;

example/src/pages/Main/ItemList.jsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@ PaginationButton.propTypes = {
1919
children: T.node.isRequired,
2020
};
2121

22-
function ItemList({ items, isLoading, error, nextPage, previousPage }) {
22+
function ItemList({ items, isLoading, error, nextPage, previousPage, onSelect }) {
2323
return (
2424
<Panel className="grid grid-rows-[1fr_min-content] p-4">
2525
<div className="overflow-x-clip">
2626
<H2>Item List</H2>
2727
{isLoading && <p>Loading...</p>}
2828
{error && <p>{error}</p>}
2929
{items && (
30-
<ul>
31-
{items.features.map(({ id }) => (
32-
<li key={id}>{id}</li>
30+
<ul className="space-y-2">
31+
{items.features.map((item) => (
32+
<li key={item.id}>
33+
<button onClick={onSelect(item)} className="text-pretty">
34+
{item.id}
35+
</button>
36+
</li>
3337
))}
3438
</ul>
3539
)}
@@ -52,6 +56,7 @@ ItemList.propTypes = {
5256
error: T.string,
5357
previousPage: T.func,
5458
nextPage: T.func,
59+
onSelect: T.func,
5560
};
5661

5762
export default ItemList;

0 commit comments

Comments
 (0)