Skip to content

Comments

feat: generate useMutation hooks for TanStack React Query plugin#3432

Open
nmokkenstorm wants to merge 1 commit intohey-api:mainfrom
nmokkenstorm:feat/use-mutation-hooks
Open

feat: generate useMutation hooks for TanStack React Query plugin#3432
nmokkenstorm wants to merge 1 commit intohey-api:mainfrom
nmokkenstorm:feat/use-mutation-hooks

Conversation

@nmokkenstorm
Copy link

@nmokkenstorm nmokkenstorm commented Feb 22, 2026

I wanted to expose react/tanstack query mutations in the same way the useQuery works, so now you get useFooMutation() , no need to wire that through together. It greatly simplifies the consumption of mutation related code in a (private/corporate) codebase that I contribute to. We've been running a local fork with this feature.

The generated useMutation hooks reduce boilerplate in application/consumer code and save lot on small files that do manual wiring:


Before:

import { runStartMutation, runStopMutation } from './generated';
import { useMutation } from '@tanstack/react-query';

const { mutate: startRun, isPending: isStarting } = useMutation({ ...runStartMutation() });
const { mutate: stopRun, isPending: isStopping } = useMutation({ ...runStopMutation() });

After:

import { useRunStart, useRunStop } from './generated';

const { mutate: startRun, isPending: isStarting } = useRunStart();
const { mutate: stopRun, isPending: isStopping } = useRunStop();

Before:

import { connectMachineMutation } from './generated';
import { useMutation, useQueryClient } from '@tanstack/react-query';

const connectMutation = useMutation({
  ...connectMachineMutation(),
  onSuccess: async () => {
    await queryClient.invalidateQueries({ queryKey: getStatusQueryKey() });
  },
});

After:

import { useConnectMachine } from './generated';
import { useQueryClient } from '@tanstack/react-query';

const connectMutation = useConnectMachine({
  onSuccess: async () => {
    await queryClient.invalidateQueries({ queryKey: getStatusQueryKey() });
  },
});

One choice/tradeoff I made was to put the mutation options as a first positional argument, as the resulting mutation function seemed the neater way to pass in actual function arguments:

// `requestOptions: true`
const { mutate, isPending } = useCreateItem(
  { onSuccess: () => navigate('..') },
  { headers: { 'X-Custom': 'value' } },
);

Without this ordering, every consumer that wants to pass onSuccess has to write:

const { mutate, isPending } = useCreateItem(undefined, {
  onSuccess: () => navigate('..'),
});

which was the more naive implementation I had at first, but the undefined everywhere for the positional arguments was bothering me. Now you set up your request stuff in the hook call, and request data in the execution of the resulting function, which seemed the more ergonomic approach.

This PR is first of three, as mentioned I am running a fork with some additional things for my project. I want use this as a starting point to get some discussion going and figure out how to collaborate / if a PR like this is appreciated or not. The actual code diff isn't that big, it's mostly wiring through a parameter that works the same way for useQuery and the snapshot tests.

@bolt-new-by-stackblitz
Copy link

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

@pullfrog
Copy link

pullfrog bot commented Feb 22, 2026

Leaping into action...

Pullfrog  | View workflow run | Using OpenCode | Triggered by Pullfrogpullfrog.com𝕏

@changeset-bot
Copy link

changeset-bot bot commented Feb 22, 2026

⚠️ No Changeset found

Latest commit: e4f4189

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@vercel
Copy link

vercel bot commented Feb 22, 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.

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. feature 🚀 Feature request. labels Feb 22, 2026
@nmokkenstorm
Copy link
Author

⚠️ No Changeset found

Latest commit: e4f4189

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
This PR includes no changesets

Click here to learn what changesets are, and how to add one.

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

It probably should actually? It's a non-breaking feature addition, happy to take some pointers re: changesets setup.

@codecov
Copy link

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 8.10811% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.31%. Comparing base (8178f61) to head (e4f4189).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...src/plugins/@tanstack/query-core/v5/useMutation.ts 12.50% 17 Missing and 4 partials ⚠️
...api-ts/src/plugins/@tanstack/react-query/config.ts 0.00% 8 Missing and 2 partials ⚠️
...i-ts/src/plugins/@tanstack/query-core/v5/plugin.ts 0.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3432      +/-   ##
==========================================
- Coverage   39.37%   39.31%   -0.07%     
==========================================
  Files         475      476       +1     
  Lines       17454    17491      +37     
  Branches     5276     5286      +10     
==========================================
+ Hits         6873     6876       +3     
- Misses       8498     8525      +27     
- Partials     2083     2090       +7     
Flag Coverage Δ
unittests 39.31% <8.10%> (-0.07%) ⬇️

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.

Copy link
Member

@mrlubos mrlubos left a comment

Choose a reason for hiding this comment

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

Not much I can say, this pull request looks very clean!

case: plugin.config.case ?? 'camelCase',
enabled: false,
name: 'use{{name}}',
requestOptions: false,
Copy link
Member

Choose a reason for hiding this comment

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

This line is redundant I think?

Suggested change
requestOptions: false,

Copy link
Author

Choose a reason for hiding this comment

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

hmmmm I think I should just remove that option I think, I originally had some configurability but I think it just needs to go.

I'll take it out, it'll be consistent with the useQuery option.

*
* @default false
*/
useMutation?:
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should have useQuery and useMutation for any other frameworks?

Copy link
Member

Choose a reason for hiding this comment

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

To clarify, this is NOT a request for this pull request, just an observation

Copy link
Author

Choose a reason for hiding this comment

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

I think the better thing is to take them both out for the other frameworks tbh, but adding the option in in the type is the consistent thing for now.

I wouldn't have added it in if the type check didn't fail on it. Agreed that changing the structure to accommodate did seem out of scope for this PR, definitely open to workshopping something later.

defaultValue: {
case: plugin.config.case ?? 'camelCase',
enabled: false,
name: 'use{{name}}',
Copy link
Member

Choose a reason for hiding this comment

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

Thoughts on this to stay in sync with the use{{name}}Query pattern below?

Suggested change
name: 'use{{name}}',
name: 'use{{name}}Mutation',

Copy link
Author

Choose a reason for hiding this comment

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

Ah yes agreed, I'll change it. I think the default of looking at the operation name was enough for my codebase to be clear but I don't think that generalises well and a suffix is objectively more consistent 👍

Copy link
Member

Choose a reason for hiding this comment

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

Did you see projects like https://github.com/7nohe/openapi-react-query-codegen or other generators? I think the ideal state is this plugin would be to eventually cover most of the extra functionality they offer. You mentioned you have multiple pull requests, what are the other ones about?

Copy link
Author

Choose a reason for hiding this comment

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

the fork I run locally has three features that are not supported in the latest release, one is what we talk about here. the others:

  • expose the rest of the query options through usequery, see here
  • allow skiptoken through that, this allows for typesafe conditional query execution without hard casts, but definitely requires opt-in. it's not a breaking change (I think?) but it does have a potential performance/bundle size impact, see here

the public github is not as clean as I'd like because I did some ad-hoc extraction, cleaning it up and making it work within the ecosystem of the public heyapi github repository is non-trivial effort so I wanted to test the waters first with the current PR we're disccusing.

if you want and timezones/agenda allows I'm also more than open to a more direct chat than github comments. I saw you're on bsky, I'm there as mokkenstorm.dev

@nmokkenstorm
Copy link
Author

Not much I can say, this pull request looks very clean!

Thank you 🫶🏻 it's a relatively simple change that composes well and saves my personally quite a bit of boilerplate in application code so I thought I'd share.

I do need a bit of time to figure out the testing strategy/merge checks, lmk if you have any tips there?

@mrlubos
Copy link
Member

mrlubos commented Feb 24, 2026

@nmokkenstorm It looks good to me. You can run tests locally if you'd like to confirm. That heavy typecheck step is flaky in CI, I'm meant to split those tests because it's gotten too large, but other things are more important

@nmokkenstorm
Copy link
Author

I'll fix what we discussed, run some local tests, report what I did and then we can continue 👍

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.

2 participants