Skip to content

Conversation

@AliceR
Copy link
Member

@AliceR AliceR commented Oct 23, 2025

Close #25

This PR migrates all data fetching hooks (useCollections, useItem, useStacApi, useStacSearch) to use TanStack Query for caching and state management, replacing the previous manual fetch implementation.

Architecture

  • All hooks now use TanStack Query under the hood for automatic caching, request deduplication, and background refetching
  • StacApiProvider now includes a QueryClientProvider to manage the React Query client
  • BREAKING CHANGE: Remove collections, setCollections, getItem, addItem,
    deleteItem from context. Context now only provides stacApi. Users should
    use useCollections() hook directly instead of accessing collections from
    context.
  • BREAKING CHANGE: Hooks no longer return state property. Use isLoading
    and isFetching booleans instead.
  • BREAKING CHANGE: reload() method renamed to refetch() in useCollection,
    useCollections, and useItem hooks.

Documentation

  • Added architecture decision record for fetch library choice
  • Updated README with peer dependency requirements and setup instructions
  • Added example implementation in example app

Migration Notes
This change adds @tanstack/react-query as a peer dependency. Consuming applications must install it!
For details, please note the migration guide as well: docs/MIGRATION.md

- Refactor useCollections and the closely related useCollection hooks to
   use TanStack Query for caching and fetching
- Improve error propagation, keep original loading states
- Update StacApiProvider to manage QueryClient and support custom clients
- Clarify peer dependency requirements for @tanstack/react-query in README
   and docs/react-query-setup.md
- Update ESLint config and package.json for new dependencies
- Minor fixes to context and tests for compatibility
@AliceR AliceR requested a review from ifsimicoded October 23, 2025 17:04
- Expose QueryClient on window for integration with TanStack Query DevTools (browser extension)
- Documented alternative: TanStack Query Devtools floating/embedded component (see https://tanstack.com/query/latest/docs/framework/react/devtools)
- Chose browser extension to keep project dependencies clean and offload devtools responsibility to the developer
- Refactor useItem to use TanStack Query for caching and fetching
- Improve error propagation, keep original loading states
Adds an ItemDetails component to the example app, showing how to use the
useItem hook to fetch and display STAC item details.
Improves documentation for consumers of stac-react.
- Refactor useStacApi to use TanStack Query for fetching and caching
- Add QueryClientProvider to wrapper to ensure a clean instance for every test
- Refactor useStacSearch.test.ts to use setupStacSearch helper for all tests, adding a step to wait for IDLE stacApi
- Refactor useStacSearch to use TanStack Query for fetching and caching
- Replace manual state management with useQuery for search and pagination
- Add FetchRequest type to handle both search POST and pagination GET requests
- Improve pagination handling to properly extract and manage next/prev links
- Clear pagination links only when API changes, not on every new search
- Add queryClient.invalidateQueries on API instance change for cache cleanup
- Fix type casting in useItem reload function for consistency
- Update tests to wrap pagination link assertions in waitFor to prevent race conditions
- Maintain backwards compatibility with existing loading states and API
…er initialization order

- Add @tanstack/react-query to vite external dependencies to prevent bundling
- Refactor StacApiProvider to split into inner/outer components, ensuring QueryClient is available before any React Query hooks execute
- Add @tanstack/react-query peer dependency to example app package.json
- Add insertTypesEntry: true to vite-plugin-dts config to generate index.d.ts
- Ensures TypeScript can resolve module declarations when package is linked or installed
@AliceR AliceR marked this pull request as ready for review November 18, 2025 11:24
Copy link

@dzole0311 dzole0311 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AliceR I gave a first pass through the PR and left a few comments below

- Add centralized query key generators in src/utils/queryKeys.ts
- Extract minimal parameters instead of hashing full request objects
- Update all hooks to use optimized query key generators

Reduces hashing costs for React Query cache lookups by generating smaller,
more stable keys with only search-relevant parameters.
- Add enableDevTools prop to StacApiProvider (defaults to false)
- Remove automatic DevTools exposure in development mode
- Make window.__TANSTACK_QUERY_CLIENT__ optional to prevent type conflicts
- Update example app to enable DevTools in development

This prevents the library from overwriting QueryClient instances in consuming
applications while still allowing opt-in DevTools support.
Clarifies that gcTime (renamed from cacheTime in TanStack Query v5) controls
inactive query memory retention and why it's set to 0 in tests.
Comment on lines +22 to +29
const response: Response = await stacApi.getCollections();
if (!response.ok) {
let detail;
try {
detail = await response.json();
} catch {
detail = await response.text();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like all of this handling should be done by a generic fetch function that's reused for all the queries, o that the response is always the same.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this suggestion. The error handling is only ~10 lines and it's duplicated in 3 places. IMO the abstraction cost outweighs the benefit:

  • Current code is clear - You can see exactly what happens in each hook
  • Stack traces are direct - No jumping through utility wrappers
  • Easy to customize - If one hook needs different error handling, it's trivial
  • Minimal duplication - Only 3 occurrences, not 30

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the abstraction will make the code more complicated. Today there are 3 occurrences, but as this gets developed, patterns are copied and we're setting a bad example. It seems that the current approach is to have the api request formulation happen in the api file so it makes sense to move this there.

@AliceR AliceR changed the title Use a fetch library for caching Migrate to TanStack Query for Data Fetching and Caching Nov 26, 2025
- Create ApiError class extending Error in src/utils/ApiError.ts
- Rename ApiError type to ApiErrorType to avoid naming conflict
- Update all hooks to throw new ApiError instances
@AliceR AliceR force-pushed the issue#25/fetch-caching branch from 5a59d65 to 0b4bcdc Compare November 27, 2025 11:09
…ient

- Remove queryClient prop from StacApiProviderType
- Detect parent via QueryClientContext instead of accepting prop
- Update docs to show QueryClientProvider wrapping pattern
- Document missing options and enableDevTools props in API reference

This simplifies the API and prevents duplicate QueryClient instances.
Add tests for StacApiProvider covering QueryClient management, DevTools
integration, and context provisioning.

- Creates QueryClient when no parent exists
- Uses parent QueryClient without nesting providers
- DevTools respects enableDevTools prop
- Provides stacApi and context methods
BREAKING CHANGE: Remove collections, setCollections, getItem, addItem,
deleteItem from context. Context now only provides stacApi. Users should
use useCollections() hook directly instead of accessing collections from
context.

React Query cache is now the single source of truth for data.
- Configure a custom QueryClient with specific caching strategies
- Pass the queryClient instance to StacApiProvider via prop

This change clarifies how to set up and use a custom QueryClient,
and investigates the impact of multiple QueryClientProviders on app behavior.
- Update example with staleTime and gcTime, and enableDevTools usage
- Document queryClient, options, and enableDevTools props in Component Properties
- Fix typo: items.description -> item.description in useItem example
- Fix malformed Error type table markdown
BREAKING CHANGE: Hooks no longer return `state` property. Use `isLoading`
and `isFetching` booleans instead.

- Update hooks to return React Query's isLoading/isFetching directly
- Update all tests to check isLoading instead of state
- Update example app (Main/index.jsx, ItemDetails.jsx) to use isLoading
- Update README with new API documentation

Eliminates duplicate state management and provides more granular loading control.
…point

- Add getCollection(collectionId) method to StacApi class
- Refactor useCollection to query /collections/{collectionId} directly
- Add generateCollectionQueryKey for independent collection caching
- Update tests to mock single collection endpoint instead of filtering

Previously, useCollection fetched all collections and filtered to find one.
Now it uses the STAC spec's dedicated endpoint, improving efficiency for
large catalogs and enabling independent collection caching.
- Change reload return type from () => void to () => Promise<QueryObserverResult>
- Remove incorrect type casts from refetch in useCollection, useCollections, useItem
- Update tests to await reload() calls with proper act() wrapping
- Remove eslint-disable comment from useItem test
- Remove debounce wrapper from useCollections reload (was returning void)

The refetch function from React Query returns a Promise, not void. This fix
properly reflects the async nature of reload operations and resolves React
act() warnings in tests. Debouncing was removed from useCollections.reload
as it's uncommon to debounce explicit reload actions.
BREAKING CHANGE: reload() method renamed to refetch() in useCollection,
useCollections, and useItem hooks.

- Rename reload() to refetch() in useCollection, useCollections, and useItem
- Update all tests to use refetch() instead of reload()
- Maintain proper async typing with Promise<QueryObserverResult>

Using refetch aligns with React Query's naming convention and makes the API
more consistent with the underlying library. The function returns the Promise
from React Query's refetch directly.

Migration: Replace .reload() with .refetch() in your code.
- Changed dateRangeFrom and dateRangeTo from required to optional in return type
- Updated state initialization to use undefined instead of empty strings
- Updated setters to accept optional string parameters
- Updated reset function to set undefined instead of empty strings

This resolves type inconsistencies where the return type claimed required strings
but the implementation initialized with empty strings. Conceptually, users should
be able to clear/unset date filters by passing undefined.
- Add url field to ApiError class for better debugging context
- Add try/catch blocks around all response.json() calls in success paths
- Include original error messages in JSON parsing error details
- Update ApiErrorType to include optional url field

Previously, if a server returned a successful status (200) but invalid JSON
(e.g., HTML error pages, malformed responses), the hooks would throw generic
parse errors. Now they throw structured ApiError instances with context about
what went wrong and which URL failed.
- Add cancel method to debounce utility function with proper TypeScript typing
- Add cleanup effect in useStacSearch to cancel pending debounced calls
- Prevent orphaned timeouts when component unmounts or _submit function changes

Previously, when _submit changed, a new debounced function was created but
the old one's timeout wasn't cancelled, causing potential memory leaks and
race conditions where stale searches could execute after newer ones.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants