Skip to content

feat(plugin-react-query): add pathParamsAsGetters option#139

Open
efpatti wants to merge 6 commits into
kubb-labs:mainfrom
efpatti:feat/path-params-as-getters
Open

feat(plugin-react-query): add pathParamsAsGetters option#139
efpatti wants to merge 6 commits into
kubb-labs:mainfrom
efpatti:feat/path-params-as-getters

Conversation

@efpatti
Copy link
Copy Markdown

@efpatti efpatti commented May 12, 2026

Tracks #140

🎯 Changes

Adds an opt-in pathParamsAsGetters: boolean option to @kubb/plugin-react-query that widens generated useQuery path-param signatures so callers can pass either a value or a zero-arg getter (T | (() => T) | undefined). Inside the hook body, each path param is unwrapped once via typeof v === 'function' ? v() : v before it is forwarded to queryKey(...) and queryOptions(...).

Motivation: reactive frameworks where reading a value at hook-call time captures only the initial snapshot warn or silently break with the current path-param-by-value shape — Svelte 5 $state (which emits state_referenced_locally), Solid signals, MobX observables, Preact signals, and similar. The getter form keeps the read inside a closure that re-evaluates on each access.

  • New option (plugin-react-query)
    • pathParamsAsGetters?: boolean (default false, non-breaking).
    • Generated output is byte-identical to prior releases when the option is omitted.
    • Runtime cost: a single typeof check per hook call.
    • Scope: useQuery only. Suspense, infinite, and mutation hooks are intentionally untouched (happy to extend in a follow-up).
  • Shared helper extraction (@internals/tanstack-query)
    • Adds printType and transformParamTypes. The two operations are framework-agnostic at the AST level — only the wrapper string and the per-param predicate differ between plugins.
  • Vue refactor (plugin-vue-query)
    • Drops 5 local copies of printType and 3 variants of wrapOperationParamsWithMaybeRef across Query, QueryOptions, InfiniteQuery, QueryKey, and Mutation in favour of the shared transformParamTypes helper.
    • 173 lines removed, 24 added. All Vue snapshots remain byte-identical — pure refactor with zero behavior change. Happy to split into a separate PR if you'd rather review the refactor independently.
  • Test coverage
    • Four new snapshot cases in queryGenerator.test.tsx: inline path-param, pathParamsType: 'object', paramsType: 'object', multi-path-param (GET /user/{userId}/pet/{petId}), and one byte-identical guard for an operation with no path params.
    • Direct unit tests for printType and transformParamTypes in @internals/tanstack-query so the shared helpers are covered independently of the consuming plugins.
  • Robustness
    • Backward-compatible Query.getParams signature: pathParamNames is optional with a default of an empty set.
    • The arg rewriter that bridges the path-param getter signature and the inner queryKey/queryOptions call expressions scopes shorthand expansion to {...} blocks so it does not misfire on function-argument commas (caught by the multi-path-param snapshot).
    • Regex metacharacters in path-param names are escaped before interpolation.
  • Docs
    • New ## pathParamsAsGetters section in packages/plugin-react-query/README.md with before/after example, motivation, and scope notes.
  • Validation
    • pnpm format clean (oxfmt).
    • pnpm lint 0/0 (oxlint).
    • pnpm typecheck 13 packages green.
    • pnpm test 849 passed / 54 files (840 existing + 9 new).
    • pnpm changeset status recognises both entries.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is for the docs (no release).

efpatti added 5 commits May 11, 2026 23:52
Both plugin-vue-query and plugin-react-query need to walk a
FunctionParametersNode and rewrite parameter types with an outer
wrapper. The two operations are framework-agnostic at the AST level —
only the wrapper string and the per-param predicate differ.

Lifts them into @internals/tanstack-query so future plugins (and the
existing ones, in follow-up commits) can share the primitive.

No consumers updated in this commit.
Drops five local printType copies and three local
wrapOperationParamsWithMaybeRef variants across Query, QueryOptions,
InfiniteQuery, QueryKey, and Mutation in favour of a single
transformParamTypes call from @internals/tanstack-query.

The hardcoded "skip 'options' / 'config'" branch is now expressed as
the shouldWrap predicate.

Net: 173 lines removed, 24 added. All snapshots remain byte-identical
— pure refactor with zero behavior change.
Widens generated useQuery path-param signatures to
\`T | (() => T) | undefined\` so callers can pass either the value or a
zero-arg getter. Inside the hook body, each path param is unwrapped
once via \`typeof v === 'function' ? v() : v\` before it is forwarded
to queryKey() and queryOptions().

Reactive frameworks (Svelte 5 \$state, Solid signals, MobX
observables) compile-warn when a reactive value is captured into a
closure that reads it only at init. The getter form keeps the read
inside a closure that re-evaluates on each access.

Implementation uses the transformParamTypes helper introduced in
@internals/tanstack-query (same primitive plugin-vue-query uses).

Defaults to false; output is byte-identical to before when omitted.
Scope: useQuery only — suspense, infinite, and mutation are untouched.
… harden coverage

Addresses review feedback before opening the upstream PR:

- Make `pathParamNames` optional in the `getParams` options object
  (defaults to an empty set). The previous required field broke
  external consumers of the public `Query.getParams` export when they
  upgraded without supplying the new field.
- Add a snapshot case for `pathParamsAsGetters: true` on an operation
  with no path params (`findByTags`). Verified byte-identical to the
  baseline `findByTags` snapshot, pinning the no-op short-circuit.
- Add a snapshot case for `pathParamsAsGetters: true` paired with
  `paramsType: 'object'` (everything-in-one-object form). Exercises
  the rewriter against shorthand inside the unified-object call
  expression.
- Add a changeset entry for `@kubb/plugin-vue-query` (patch) so the
  internal refactor that drops the local `wrapOperationParamsWithMaybeRef`
  and `printType` copies in favour of the shared helper is visible in
  the release notes.
…rewriter

Three optional follow-ups to the original feature:

- Add a snapshot case for an operation with two path params
  (\`GET /user/{userId}/pet/{petId}\`). Exercises the prelude emitting
  multiple unwrap lines and the rewriter substituting both names in
  the queryKey/queryOptions call expressions.
- Fix a latent bug in \`buildArgRewriter\`'s shorthand expansion: the
  previous regex matched on top-level commas as well, which made the
  multi-path-param inline call \`(userId, petId, config)\` produce the
  syntactically invalid \`(userId_, petId: petId_, config)\`. Step 1
  is now scoped to occurrences inside \`{...}\` blocks, so function
  argument commas no longer trigger shorthand expansion.
- Defensive coding: escape regex metacharacters in path-param names
  before interpolating them into the rewriter's regexes. Today path
  param names come through \`ast.caseParams\` and are always valid
  identifiers, but the escape guards against future shape changes.
- Add direct unit tests for \`printType\` and \`transformParamTypes\` in
  \`@internals/tanstack-query\`. Previously these helpers were only
  covered transitively via the Vue and React plugin snapshots; the
  new tests let the helpers be evolved or refactored in isolation
  without waiting on plugin-level test runs.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

🦋 Changeset detected

Latest commit: 209c97d

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

This PR includes changesets to release 13 packages
Name Type
@kubb/plugin-react-query Minor
@kubb/plugin-vue-query Minor
tests-3.0.x Patch
e2e Patch
@kubb/plugin-client Minor
@kubb/plugin-cypress Minor
@kubb/plugin-faker Minor
@kubb/plugin-mcp Minor
@kubb/plugin-msw Minor
@kubb/plugin-redoc Minor
@kubb/plugin-ts Minor
@kubb/plugin-zod Minor
performance Patch

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

@stijnvanhulle
Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

@efpatti
Copy link
Copy Markdown
Author

efpatti commented May 17, 2026

@copilot resolve the merge conflicts in this pull request

Hi buddy! I’ll resolve them manually to avoid breaking snapshots/shared generator logic, then rerun tests before pushing.

- Adopt upstream's queryKeyTransformer / buildQueryKeyParams API
- Migrate generator destructuring to ctx.inputNode
- Drop vue-query transformParamTypes refactor (upstream kept local helper)
- Preserve pathParamsAsGetters option, helpers, and test cases
@efpatti efpatti marked this pull request as ready for review May 17, 2026 05:23
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants