Skip to content

feat(@tanstack/query): add queryOptions override parameter to generated useQuery hooks#3528

Open
nmokkenstorm wants to merge 2 commits into
hey-api:mainfrom
nmokkenstorm:feat/use-query-options
Open

feat(@tanstack/query): add queryOptions override parameter to generated useQuery hooks#3528
nmokkenstorm wants to merge 2 commits into
hey-api:mainfrom
nmokkenstorm:feat/use-query-options

Conversation

@nmokkenstorm
Copy link
Copy Markdown
Contributor

@nmokkenstorm nmokkenstorm commented Mar 9, 2026

Summary

When useQuery hook generation is enabled, generated hooks now accept an optional queryOptions property that is spread over the computed query options, which is the same as the existing mutationOptions override on useMutation hooks. This lets consumers override TanStack Query options without losing type safety.

Independent of #3926 (skipToken support). Whichever PR merges first, the other will need a small useQuery.ts rebase to combine both features.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 9, 2026

@nmokkenstorm is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 9, 2026

🦋 Changeset detected

Latest commit: 7c1ecdb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch from 6d07fd5 to cf8e859 Compare March 9, 2026 15:55
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 39.55%. Comparing base (7885c09) to head (c2b4e46).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3528      +/-   ##
==========================================
+ Coverage   39.22%   39.55%   +0.33%     
==========================================
  Files         595      596       +1     
  Lines       21124    21139      +15     
  Branches     6151     6153       +2     
==========================================
+ Hits         8285     8362      +77     
+ Misses      10406    10354      -52     
+ Partials     2433     2423      -10     
Flag Coverage Δ
unittests 39.55% <100.00%> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 9, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3528

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3528

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3528

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3528

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3528

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3528

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3528

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3528

commit: c2b4e46

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Mar 9, 2026

@nmokkenstorm Why is it an optional configuration flag? Why not add it for everyone?

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Mar 9, 2026

@nandorojo will like this

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Mar 9, 2026

@nmokkenstorm I updated your description with linked issue

@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@nmokkenstorm Why is it an optional configuration flag? Why not add it for everyone?

habit to make it opt-in tbh. I think the skipToken is a breaking change too on type level and it also slightly increases the generated bundle size but I will check.

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Mar 9, 2026

@nmokkenstorm I didn't look at the code yet, stopped after seeing the configuration haha. If it's going to be breaking either way, I'd much rather deprecate the old approach because we wouldn't want new users to start with the flag disabled, realize one day they need to enable it, and face a lot of breaking changes. We can talk through it when you feel the pull request is more ready. I'm pretty sure there was another thread where we talked about skip tokens but can't find it, I'd like to ideally avoid another breaking change when that thread gets addressed

@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@nmokkenstorm I didn't look at the code yet, stopped after seeing the configuration haha. If it's going to be breaking either way, I'd much rather deprecate the old approach because we wouldn't want new users to start with the flag disabled, realize one day they need to enable it, and face a lot of breaking changes. We can talk through it when you feel the pull request is more ready. I'm pretty sure there was another thread where we talked about skip tokens but can't find it, I'd like to ideally avoid another breaking change when that thread gets addressed

I'll double check, I think it's definately doable to have it be backwards compatible without a breaking change. The useQuery/Mutation stuff was opt in so I stuck to the pattern but I think this can be done without losing ergonomics.

I'll update the pr description and ping you if it's reviewable, I needed a remote public ref to do some internal testing with first anyways.

Thanks for the feedback!

@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch from 7df42fb to 3007b3c Compare March 10, 2026 08:27
@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Mar 10, 2026

@nmokkenstorm Can you clarify what do you mean by "add useQuery hooks" in the title? React/Preact Query already support generating useQuery hooks so I'm confused what it's referencing

@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@nmokkenstorm Can you clarify what do you mean by "add useQuery hooks" in the title? React/Preact Query already support generating useQuery hooks so I'm confused what it's referencing

its about having the generated useQuery hooks exist per entity, with queryoptions and skiptoken enabled. the reason it's named like that is because the original code/branch orginates from a fork that pre-dates the support we had in our project.

still tinkering on this, nothing to see/review here yet!

@nmokkenstorm nmokkenstorm changed the title [draft] add useQuery hooks and skipToken support [draft] expose query options and skiptoken on generated useQuery hooks Mar 10, 2026
@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Mar 10, 2026

Just making sure we're aligned on the scope! I shall retreat once more

@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch 3 times, most recently from d6ebc7c to b1fc4b9 Compare March 20, 2026 08:55
@nmokkenstorm nmokkenstorm changed the title [draft] expose query options and skiptoken on generated useQuery hooks feat: add skipToken support and queryOptions override to TanStack Query useQuery hooks Mar 20, 2026
@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch from b1fc4b9 to 2ffe100 Compare March 24, 2026 15:48
@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch 5 times, most recently from d8bbba0 to 5eb6a92 Compare April 3, 2026 11:04
@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@mrlubos I think it does what it needs to do now, below a redacted diff of how we're currently using it in the fork.

I ran into an issue where the auto paginated version of useQuery has ts-ignore inserted and I needed it somewhere else to stay compliant, so I added support for a new case to the DSL for that. Probably worthwhile to check a) if that's correct and b) if maybe we can improve the related code to no longer need the ts-ignore, but that seemed a bit out of scope for this PR.

lmk what you think? I'll see if I can get the merge checks to behave a bit more but I'm not sure what I actually can and can not influence there, you mentioned they are a bit flaky before.


Before (query)

import { getSomeResourceOptions } from '@org/api';
import { useQuery } from '@tanstack/react-query';

const { data, isLoading } = useQuery({
  ...getSomeResourceOptions({ path: { id: id ?? '' } }),
  enabled: !!id,
});

After (query)

import { useGetSomeResource } from '@org/api';
import { skipToken } from '@tanstack/react-query';

const { data, isLoading } = useGetSomeResource(
  id ? { path: { id } } : skipToken
);

Before (query with extra options)

import { getStatusOptions } from '@org/api';
import { useQuery } from '@tanstack/react-query';

const { data, isLoading } = useQuery({
  ...getStatusOptions(),
  enabled: shouldPoll,
  refetchInterval: shouldPoll ? 5000 : false,
});

After (query with extra options)

import { useGetStatus } from '@org/api';

const { data, isLoading } = useGetStatus({
  queryOptions: {
    enabled: shouldPoll,
    refetchInterval: shouldPoll ? 5000 : false,
  },
});

Before (mutation)

import { updateResourceMutation, getResourceQueryKey } from '@org/api';
import { useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
  ...updateResourceMutation(),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: getResourceQueryKey() });
  },
});

After (mutation)

import { useUpdateResource, getResourceQueryKey } from '@org/api';
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();
const { mutate, isPending } = useUpdateResource({
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: getResourceQueryKey() });
  },
});

@nmokkenstorm nmokkenstorm marked this pull request as ready for review April 7, 2026 09:17
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 7, 2026

Reviewed PR #3528. The skipToken support and queryOptions override implementation is well-structured and type-safe. Flagged the useQuery default change from false to true as a breaking change that needs explicit acknowledgment, and noted a dead symbol registration (UseQueryOptions/CreateQueryOptions) in plugin.ts.

Task list (5/5 completed)
  • Read diff to understand the scope of changes
  • Review core implementation files (plugin, useQuery, queryOptions, config, types)
  • Review test changes
  • Check snapshot changes for correctness
  • Self-critique and submit review

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch 2 times, most recently from 2071133 to b0ccb2b Compare April 8, 2026 13:16
@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

nmokkenstorm commented Apr 10, 2026

@mrlubos all green except the vercel thing. I have some redacted real world usage before/after below, happy to chat if you have questions/concerns!


Generated output: *Options factory

Before no skipToken support, consumers must guard externally:

export const getItemOptions = (options: Options<GetItemData>) =>
  queryOptions({
    queryFn: async ({ queryKey, signal }) => {
      const { data } = await getItem({
        ...options,
        ...queryKey[0],
        signal,
        throwOnError: true,
      });
      return data;
    },
    queryKey: getItemQueryKey(options),
  });

After skipToken flows through naturally:

export const getItemOptions = (options: Options<GetItemData> | typeof skipToken) => {
  const opts = options !== skipToken ? options : undefined;
  return queryOptions({
    queryFn: options === skipToken
      ? skipToken
      : async ({ queryKey, signal }) => {
          const { data } = await getItem({
            ...opts,
            ...queryKey[0],
            signal,
            throwOnError: true,
          });
          return data;
        },
    queryKey: getItemQueryKey(opts as Options<GetItemData>),
  });
};

Generated output: new useQuery hook

Before not generated, consumers wire up useQuery + *Options manually.

After generated hook with skipToken + queryOptions override:

export const useGetItem = (
  options?: (Options<GetItemData> | typeof skipToken) & {
    queryOptions?: Partial<Omit<ReturnType<typeof getItemOptions>, 'queryKey' | 'queryFn'>>;
  },
) => {
  if (options === skipToken) {
    return useQuery(getItemOptions(options));
  }
  const { queryOptions, ...sdkOptions } = options ?? {};
  return useQuery({ ...getItemOptions(sdkOptions), ...queryOptions });
};

Consumer code

1. Conditional fetch with skipToken

Before (manual enabled guard):

const query = useQuery(getItemOptions({ path: { id: id ?? '' } }));
// had to also pass enabled: !!id separately

After (just pass skipToken):

const { data, isLoading, error } = useGetItem(
  id?.trim() ? { path: { id } } : skipToken,
);

2. queryOptions override for route-param gating

const itemQuery = useGetItem({
  path: { id: itemId ?? '' },
  queryOptions: { enabled: !!itemId },
});

3. *Options factories with useQueries for fan-out

const itemsQuery = useGetItems();
const items = itemsQuery.data ?? [];

const detailQueries = useQueries({
  queries: items.map((item) =>
    getItemDetailOptions({ path: { id: item.id } }),
  ),
});

4. Mutation + *QueryKey for targeted invalidation

const listQuery = useGetItemList(parentId ? { path: { parentId } } : skipToken);
const items = listQuery.data ?? [];

const detailQueries = useQueries({
  queries: items.map((item) =>
    getItemDetailOptions({ path: { id: item.id } }),
  ),
});

const updateMutation = useUpdateItem({
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: getItemListQueryKey({ path: { parentId: parentId ?? '' } }),
    });
  },
});

@mrlubos mrlubos self-assigned this Apr 20, 2026
@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 20, 2026

@pullfrog review

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 20, 2026

Reviewed PR #3528. The implementation is clean and correct — skipToken support is properly wired through queryOptions, infiniteQueryOptions, and the new useQuery hooks for React/Preact. The useQuery: false opt-out works as expected, and non-hook frameworks (Angular, Solid, Svelte, Vue) correctly skip useQuery generation while still gaining skipToken support in their query options. No issues found.

Task list (5/5 completed)
  • Read diff - source code changes (plugin logic, config, types)
  • Read diff - test changes
  • Read diff - snapshot changes (sampling)
  • Trace data flow and verify correctness
  • Self-critique and submit review

Pullfrog  | View workflow run | via Pullfrog𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Reviewed — no issues found.

Task list (5/5 completed)
  • Read diff - source code changes (plugin logic, config, types)
  • Read diff - test changes
  • Read diff - snapshot changes (sampling)
  • Trace data flow and verify correctness
  • Self-critique and submit review

Pullfrog  | View workflow run𝕏

@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@mrlubos I see some conflicts have crept in, happy to resolve them; do you need anything else to get this reviewed / merged while I'm at it?

@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch 2 times, most recently from 51a80e4 to a791fdd Compare May 5, 2026 16:08
@DogPawHat
Copy link
Copy Markdown

DogPawHat commented May 22, 2026

All in favor of the skip token work thanks.

One question: this auto-generates query hooks based on the query options and make that behavior the default. I'd actually prefer that the hooks NOT be generated by default. The query options do mostly everything we need and I think auto generated custom hooks are now abstraction dead weight in my opnion. To quote from https://tkdodo.eu/blog/the-query-options-api#query-factories

I would even take it to a point where custom hooks won’t be my first choice for abstractions. They seem a bit pointless if all they do is: const useTodos = () => useQuery(todosQuery)

Perfectly fine having the option to generate hooks but it's extra code that I think people should ask for directy.

@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

All in favor of the skip token work thanks.

No problem!

One question: this auto-generates query hooks based on the query options and make that behavior the default.

This was the original behaviour, there was some discussion about this upthread.

I sort of I agree with what you're saying for different reasons, I think in an ideal scenario I either import the hook straight up from the generated file because I'm not doing something complicated. If I'm doing something complicated with the custom options (and probably/possibly reusing that throughout my app) I specific it separately by introducing a new file.

I agree with you that creating a file just for re-exporting the generated hook doesn't make for the best ergonomics.

@mrlubos and I had some talk here and elsewhere about BC and why the current approach makes sense, but I must say it has been a while and I reworked this code a bit over time so maybe if we talk again we might get to a different conclusion

Either way thanks for your insight! Happy to work a bit more on this if needed to get it merged, once in a while I rebase with main and fix any conflicts/issues that surface, I'd be more than happy to receive some instruction from the maintainers to see what we can do to get this in. Fwiw the project I'm working on that's appreciating the missing/added functionality by this PR got a lot leaner for it.

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented May 24, 2026

@nmokkenstorm is there a world where we could get the skipToken support without the hooks? Second everything else that was said above, my question is purely from the design perspective.

@DogPawHat
Copy link
Copy Markdown

@nmokkenstorm is there a world where we could get the skipToken support without the hooks? Second everything else that was said above, my question is purely from the design perspective.

I was a bit confused why the two where coupled together as well

@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@nmokkenstorm is there a world where we could get the skipToken support without the hooks? Second everything else that was said above, my question is purely from the design perspective.

Hmmm they're not necessarily intrinsically linked, although it does make sense to implement them at least in a way that behaves well together.

The way the skiptoken works is sort of as an override/overload to the query options, so any implementation for either should at least be aware of both. I can split it up in two separate (stacked?) PRs and explain the design decisions behind them if that makes reviewing easier.

@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch from 291119b to 6ce8096 Compare May 25, 2026 12:14
@nmokkenstorm nmokkenstorm changed the title feat: add skipToken support and queryOptions override to TanStack Query useQuery hooks feat(@tanstack/query)!: enable useQuery hooks by default with queryOptions override May 25, 2026
@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch from 6ce8096 to 2410551 Compare May 25, 2026 13:19
@nmokkenstorm nmokkenstorm marked this pull request as draft May 25, 2026 13:20
@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch 3 times, most recently from 80d2e32 to e57f9a2 Compare May 25, 2026 16:38
@nmokkenstorm nmokkenstorm force-pushed the feat/use-query-options branch from e57f9a2 to c2b4e46 Compare May 25, 2026 16:51
@nmokkenstorm nmokkenstorm changed the title feat(@tanstack/query)!: enable useQuery hooks by default with queryOptions override feat(@tanstack/query): add queryOptions override parameter to generated useQuery hooks May 25, 2026
@nmokkenstorm
Copy link
Copy Markdown
Contributor Author

@nmokkenstorm is there a world where we could get the skipToken support without the hooks? Second everything else that was said above, my question is purely from the design perspective.

@mrlubos @DogPawHat it's split now, slightly unsure on the branch hygiene, would love you input. this branch is now query options only, the linked branch is skiptoken, I have half a thought to retarget this PR to the other branch, but I don't know how well that works with your review flow. Alternative is to just merge the other branch first, keep this in draft and I do the integration of both features.

Either way both need some additional testing on my end because the code diverges slightly from the original approach, but I think especially the skiptoken stuff should be fairly straightforward to test/review, lmk if you run into any issues!

# Conflicts:
#	packages/openapi-ts/src/plugins/@tanstack/query-core/v5/useQuery.ts
@nmokkenstorm nmokkenstorm marked this pull request as ready for review May 29, 2026 14:02
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Reviewed changes — adds a queryOptions override parameter to generated useQuery hooks, mirroring the existing mutationOptions pattern on useMutation hooks. The change lets consumers pass TanStack Query options like staleTime or enabled per call site without losing type safety.

  • Introduce UseQueryParams<TFactory> type alias in packages/openapi-ts/src/plugins/@tanstack/query-core/shared/useQueryParams.ts — a shared utility type that intersects the SDK options with an optional queryOptions override typed as Partial<Omit<ReturnType<TFactory>, 'queryKey' | 'queryFn'>>.
  • Update createUseQuery emitter in packages/openapi-ts/src/plugins/@tanstack/query-core/v5/useQuery.ts — replaces the direct Options<TData> parameter with UseQueryParams<typeof optionsFn>, destructures queryOptions from the parameter, and spreads it after the computed options so overrides take precedence.
  • Add unit test in packages/openapi-ts/src/plugins/@tanstack/query-core/v5/__tests__/useQuery.test.ts — verifies the emitted AST contains the type alias and the correct hook body.
  • Add snapshot test scenario useQuery-enabled across OpenAPI 2.0.x, 3.0.x, and 3.1.x — covers generated output when useQuery: true is set in plugin config.
  • Add changeset for a minor version bump of @hey-api/openapi-ts.

Pullfrog  | View workflow run | Using Kimi K2𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants