Our current data-fetching pattern has several maintainability issues:
- Verbose and abused
useEffect blocks — every component that needs data from ExtensionRegistryService must manually convert Promises into state variables (isLoading, error, data). This leads to useEffect being overused as a general-purpose async mechanism. The result is components that are hard to read, hard to test, and prone to side-effect bugs.
- Manual cancellation —
AbortController signals need to be manually threaded through each call, which is fragile and difficult to debug when something goes wrong.
- Unsafe retry behavior —
fetch-retry wraps all requests including write mutations, which get silently retried up to 10 times on failure. This is dangerous for non-idempotent operations and invisible to the user.
- No path to reliable testing — because data-fetching is tightly coupled to the
ExtensionRegistryService with no provider abstraction, there is no clean way to mock network behavior in unit or integration tests. TanStack Query's QueryClientProvider is the industry standard solution to this: in production the app uses a real client, and in tests you swap it for a mock one, enabling both hook-level and full component tree testing without any real network calls.
Proposed solution
Adopt TanStack Query (@tanstack/react-query) as the standard data-fetching layer. It handles loading/error state, caching, request deduplication, automatic cancellation, and retry logic out of the box — with sensible defaults (reads retry, mutations do not).
As a future improvement, we could consider leveraging React Error Boundaries alongside TanStack Query's, which lets fetch failures propagate as render errors and be caught at a boundary rather than handled per-component. This opens the door to consistent, centralized error UI without any additional per-hook wiring.
I've created a POC replacing the search query with that #1763
Our current data-fetching pattern has several maintainability issues:
useEffectblocks — every component that needs data fromExtensionRegistryServicemust manually convert Promises into state variables (isLoading,error,data). This leads touseEffectbeing overused as a general-purpose async mechanism. The result is components that are hard to read, hard to test, and prone to side-effect bugs.AbortControllersignals need to be manually threaded through each call, which is fragile and difficult to debug when something goes wrong.fetch-retrywraps all requests including write mutations, which get silently retried up to 10 times on failure. This is dangerous for non-idempotent operations and invisible to the user.ExtensionRegistryServicewith no provider abstraction, there is no clean way to mock network behavior in unit or integration tests. TanStack Query'sQueryClientProvideris the industry standard solution to this: in production the app uses a real client, and in tests you swap it for a mock one, enabling both hook-level and full component tree testing without any real network calls.Proposed solution
Adopt TanStack Query (
@tanstack/react-query) as the standard data-fetching layer. It handles loading/error state, caching, request deduplication, automatic cancellation, and retry logic out of the box — with sensible defaults (reads retry, mutations do not).As a future improvement, we could consider leveraging React Error Boundaries alongside TanStack Query's, which lets fetch failures propagate as render errors and be caught at a boundary rather than handled per-component. This opens the door to consistent, centralized error UI without any additional per-hook wiring.
I've created a POC replacing the search query with that #1763