Skip to content

Migrate to TanStack Query for Data Fetching and Caching#27

Merged
AliceR merged 32 commits intodevelopfrom
issue#25/fetch-caching
Dec 15, 2025
Merged

Migrate to TanStack Query for Data Fetching and Caching#27
AliceR merged 32 commits intodevelopfrom
issue#25/fetch-caching

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.
@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.
Copy link

@sandrahoang686 sandrahoang686 left a comment

Choose a reason for hiding this comment

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

Hey Alice, great job on all the documentation! I found the migration doc super helpful and awesome 🙌🏼 . I've reviewed but its definitely quite a large PR with a lot of meaty things so I wasn't able to review as in-depth as i'd like, it would take me a lot longer or several iterations. Hoping other team members can jump in to help review too. cc @dzole0311 @danielfdsilva @ifsimicoded.

Maybe next time it might be a good idea to do this in iterative PRs (adding tanstack query => refactoring one of the hooks => refactoring another => docs => etc) so it could be alot easier to review and merge 🙇🏼‍♀️

- Add handleStacResponse<T>() utility function to remove duplication
- Refactor useCollection, useCollections, useItem, useStacSearch to use utility
- Add test suite for handleStacResponse
- Add test suite for StacApi class methods
- Add StacHook base interface with common properties (isLoading, isFetching, error)
- Add StacRefetchFn<T> type for standardized refetch signatures
- Update all hooks to extend StacHook interface (StacCollectionHook, StacCollectionsHook, StacItemHook, StacSearchHook)
@AliceR
Copy link
Member Author

AliceR commented Dec 15, 2025

Thanks everyone for the thorough reviews! 🙏

@sandrahoang686 - All of your suggestions have been implemented, and questions answered.

@danielfdsilva - Your centralization suggestion was spot on! I went with the utility function approach (handleStacResponse<T>()) instead of moving it into the StacApi class itself. This gives us the best of both worlds:

  • Eliminated ~90 lines of duplicated parsing code
  • Hooks retain flexibility to access Response details when needed (status codes, headers, ...)
  • StacApi stays as a thin routing layer
  • Optional utility—hooks can still do custom handling if needed

All tests passing, TypeScript clean, and ready to go! 🚀

I'm merging this to develop now to enable future work in smaller PRs based on this big refactor. Looking forward to building on this foundation together!

@AliceR AliceR merged commit a4b4881 into develop Dec 15, 2025
@danielfdsilva danielfdsilva deleted the issue#25/fetch-caching branch December 16, 2025 12:14
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