Migrating an endpoint to TanStack Query
Only run this when the user explicitly asks to migrate an endpoint. The migration is in progress and opt-in — never convert endpoints as a side effect of other work.
The current picture
src/extension-registry-service.ts (ExtensionRegistryService, plus service.admin.*) holds every server call. Legacy, un-migrated consumers call these methods straight from a component, passing an AbortController and relying on fetch-retry's 10-attempt backoff inside sendRequest. That drags along per-component AbortController refs, useEffect fetch-on-mount wiring, and hand-rolled loading/error state.
Migrated endpoints instead go through a use* hook wrapping useQuery/useMutation, retries move to the shared query client (src/query-client.ts), and error handling moves into the transport via sendStrictRequest. Roughly half the service is migrated — grep before assuming either state.
Steps
Find every consumer of the method you're migrating:
grep -rn "service\.<method>\|\.<method>(" src. List them — you'll migrate all of them or a named subset.Decide the retry scope — ask if unsure. Ideally the service method flips from
sendRequest(retriable) tosendStrictRequest, handing retries to TanStack. Only do that when every consumer is moving to a hook — a legacy consumer still calling the method directly would silently lose its retry and start seeing rejections where it used to get a resolved error result. If you're migrating just one of several consumers, either leave the method retriable (the query then double-retries, tolerated in the interim) or confirm scope with the user. When the request doesn't make the consumer scope clear, ask.Adjust the service method.
- Switch it to
sendStrictRequestand drop| ErrorResultfrom its return type (Promise<Readonly<SuccessResult | ErrorResult>>→Promise<Readonly<SuccessResult>>). The method now resolves with data or rejects; the hook needs noisErrorcheck, and consumers lose theiras SuccessResultcasts. - Query methods: keep the
AbortControllerparam — the hook passescontrollerFromSignal(signal). - Mutation methods: drop the
AbortControllerparam — we no longer abort writes.
- Switch it to
Create the hook — shape and naming per the
tanstack-query-conventionsskill. Co-locate it in the feature's folder first; move tosrc/hooks/only when a second place needs it. The hook returns the react-query result object as-is, never justdataor a picked subset.Update the consumers. Replace the
AbortController/useEffect/ manual-state boilerplate with the hook, destructuring and renaming its result (const { data: user, error: userError } = ...;const { mutateAsync, isPending } = ...). Delete the dead boilerplate.Fix the tests that stubbed the old contract. A spec stubbing the service method with
mockResolvedValue({ error: '…' })was standing in for the old resolve-an-error-result behaviour — flip it tomockRejectedValue({ error: '…' }).sendStrictRequestitself is covered once, intest/unit/server-request.spec.ts; don't re-test it per endpoint.Finish per the
write-codeskill: add or update tests (write-tests), add a changelog entry, and passyarn lint.
Don't
- Don't strip
fetch-retryfromsendRequestglobally — that's the final cleanup once the whole migration is done, and un-migrated direct callers still depend on it. Keep the 429 /Retry-Afterblock regardless. - Don't migrate endpoints nobody asked you to.