Skip to content

ref(api): Let apiFetch serve as a mutationFn - #127699

Merged
ryan953 merged 8 commits into
masterfrom
ref/api-fetch-mutations-base
Oct 16, 2026
Merged

ryan953 merged 8 commits into
masterfrom
ref/api-fetch-mutations-base

Conversation

@ryan953

@ryan953 ryan953 commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

apiFetch and apiFetchInfinite now take only the context fields they read, instead of the whole QueryFunctionContext. This lets a mutationFn call apiFetch({queryKey: [url, {method, data}]}) and get the same ApiResponse ({json, headers, status}) that queries get. A mutation's own context has no queryKey or signal, and the other query fields describe a running query that a mutation would have to invent. No runtime behavior changes.

  • apiFetch takes {queryKey: ApiQueryKey}, and apiFetchInfinite takes {queryKey: InfiniteApiQueryKey, pageParam}. Passing either one as a queryFn still works, because the full context satisfies the narrower type. usePinnedLogsQuery was the one caller that built a context literal with extra fields; it now passes only the queryKey.
  • The mock API client now awaits a promise body when includeAllArgs is set. apiFetch always sets it, so a spec that keeps a request pending with a promise body got the pending promise back as json. Without includeAllArgs, resolve() already adopted the promise. One preprod spec passed only because of that leak (its "pending" size analysis was treated as loaded data); it now mocks a resolved response and lets the PROCESSING build state drive the message.

No feature flags. No UI changes, so no screenshots.

apiFetch now accepts just {queryKey} rather than a full
QueryFunctionContext, so mutations can call it directly and resolve to
the same ApiResponse ({json, headers, status}) shape the query cache
uses. queryFn usage is unchanged, since a full context still satisfies
the narrower parameter; usePinnedLogsQuery built a context literal with
extra fields and now passes only the queryKey.

The mock API client now awaits a promise body when includeAllArgs is
set. apiFetch always sets it, so specs that hold a mutation in flight
with a promise body would otherwise receive the pending promise as json.
One preprod spec passed only because of that leak and now mocks a
resolved size analysis instead.
Narrowing the parameter to just queryKey meant apiFetch could never use other context fields, even though a mutation could supply them itself (for example its own AbortSignal). Keep queryKey required and accept the rest optionally, so queries pass the full context as before and mutations pass what they have. usePinnedLogsQuery can again pass its full context literal.
Mutations have no query context to forward, and the other QueryFunctionContext fields (signal, client, meta) describe a running query that a mutation would have to invent. Accept just the queryKey that both callers can supply. usePinnedLogsQuery built a context literal with extra fields and now passes only the queryKey.
Match apiFetch: destructure the two context fields the function reads instead of accepting the whole query context.
Comment on lines +43 to +48
export async function apiFetch<TQueryFnData = unknown>({
queryKey,
}: Pick<QueryFunctionContext<ApiQueryKey>, 'queryKey'>): Promise<
ApiResponse<TQueryFnData>
> {
const [url, options] = queryKey;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

narrow the type sig so callsites don't need to pass extra fields that are ignored anyway.

Comment on lines +68 to +74
queryKey,
pageParam,
}: Pick<
QueryFunctionContext<InfiniteApiQueryKey, null | undefined | ParsedHeader>,
'queryKey' | 'pageParam'
>): Promise<ApiResponse<TQueryFnData>> {
const [url, options] = queryKey;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

narrow the type sig so callsites don't need to pass extra fields that are ignored anyway.

Comment on lines -80 to -81
signal: new AbortController().signal,
meta: undefined,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

remove extra fields that were ignored anyway.

MockApiClient.addMockResponse({
url: SIZE_ANALYSIS_URL,
method: 'GET',
body: new Promise(() => {}), // Keep pending

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The test before was broken because static/app/__mocks__/api.tsx was returning the object with type==Promise as the mocked value. So the request looked like it was fetched right away.

The fix to the mock means that in the future we will be able to hold a request in the pending state... but this test only passes when SIZE_ANALYSIS_URL returns quickly right away. The comment was maybe aspirational, but wrong.

@ryan953
ryan953 marked this pull request as ready for review October 16, 2026 01:38
@ryan953
ryan953 requested review from a team as code owners October 16, 2026 01:38
The functions only read queryKey (and pageParam), so spell those out with ApiQueryKey and InfiniteApiQueryKey instead of picking them from QueryFunctionContext.
useConcurrencyLimitedQueryFn reads signal itself to skip aborted work, but required the wrapped queryFn to accept it too. apiFetch now takes only {queryKey}, so it no longer satisfied that constraint. Require signal on the returned function, which TanStack calls with the full query context, and let the wrapped queryFn take any narrower context.
const deferred = Promise.withResolvers<TResult>();
addItem({
reject: deferred.reject,
run: async () => deferred.resolve(await queryFn(context)),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we only need to pass in TContext here queryFn(context). signal isn't required at this point

but signal is required in the surrounding code, so the types have been fiddled with.

@ryan953
ryan953 enabled auto-merge (squash) October 16, 2026 17:28
@ryan953
ryan953 merged commit e3588e4 into master Oct 16, 2026
90 checks passed
@ryan953
ryan953 deleted the ref/api-fetch-mutations-base branch October 16, 2026 17:30

This branch was successfully deployed

1 active deployment
Preview — efa8b39b Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants