Back in May of 2025, (Dominik Dorfmeister) posted an RFC on the TanStack Query repo that caught my attention:

RFC: Unified Imperative Query Methods · TanStack query · Discussion #9135
Context Sometimes, APIs don’t evolve well. I’ve seen the situation a couple of times that we add an API, and we think it’s great, and then after some time, we add another API that does something si...
https://github.com/TanStack/query/discussions/9135

I've been very interested in the imperative methods of TanStack Query for a bit, mostly for the integration with TanStack Router. As detailed in Dominik's blog posts https://tkdodo.eu/blog/tan-stack-router-and-query and https://tkdodo.eu/blog/reliable-query-prefetching-with-tanstack-router, when you are using a router or framework with query you get the opportunity to get the data being fetched ahead of when you need to render it1. However, it's been confusing for a bit as to whether to use ensureQueryData, prefetchQuery or fetchQuery, and as they all do the same thing (with ensure and prefetch wrapping fetchQuery), collapsing the methods into one method2 would make communicating how to do prefetching a lot easier.

After a while of inactivity on the RFC and with most of the feedback broadly in favor of the proposal, I asked if a PR would be welcome. Dominik agreed, and I got to work.

Shape of the problem3

The methods

I first created new methods named query and infiniteQuery in the QueryClient class of @tanstack/query-core, just as simple copies of fetchQuery and infiniteQuery.

From there, the use cases of the other two methods could be easily collapsed in:

  • prefetchQuery having always been a one-liner, recommending that people use void queryClient.query().catch(noop) became the default

  • ensureQueryData use case was covered by the staleTime: 'static' option that Dominik has already implemented

There were two more requests that Dominik asked for though:

First, have the new methods utilise the select option that could be defined with the query options helpers for useQuery, but wasn't usually used by the imperative methods up until now (so for any given queryOptions with both a queryFn and a select, useQuery/useSuspenseQuery would return the result of the select passed the result of the queryFn while fetchQuery would return the raw queryFn result)

Fix for this is resonably simple:

// check that a select was passed in
const select = defaultedOptions.select

// early return if select exists
if (select) {
 return select(queryData)
}

return queryData

Secondly, Dominik initially wanted to allow both enabled & skipToken. This was very tricky as if you were going to disable or skip a query, you could have a call a query function that wasn't actually defined, and the only reasonable UX I could think of in that case was a thrown error. This requirement eventually just got pared back to only allowing the skipToken in via the query and ignoring enabled4, which resulted in a type only change as opposed to trying to implement extra error throwing.

With the new methods done, we applied the @deprecated JSDoc tag to all of the old methods, which will give warnings in type-checkers and linters down the line.

The tests

The query-core package has a global types.ts file with all the shared types and interfaces for the rest of the package. Query's determination to first-in-class type-safety means this is a relatively complex file with a lot of generics and extends keywords.

Because of the change with accepting select, we couldn't just reuse the existing FetchQueryOptions/FetchInfiniteQueryOptions, which are used by the old methods. I settled on creating new interfaces called QueryExecuteOptions and InfiniteQueryExecuteOptions and @deprecated'ing the old interfaces.

QueryExecuteOptions was relatively straightforward; because we now accept select, we need to accept a new TData generic that's separate from TQueryData, so that select could be typed as (data: TQueryData) => TData.

A digression about query data typing
A digression about query data typing

The InfiniteQueryExecuteOptions was a bit tricky as initially I used the InfiniteQueryObserverOptions TData type as a reference, and that defaulted to TQueryFnData as well. This ended up breaking the queryOptions helpers that were defined at the adapter level; the output of those helpers should be passed to query without causing type errors, so TData has to match the helpers type of InfiniteData<TQueryFnData> instead5.

The tests

This was the biggest part of the whole project. Basically there were two things we had to do:

  • Copy tests for fetchQuery et al and have them test query/infiniteQuery for behavior that was expected to be identical.

  • Add tests for query and infiniteQuery that was new (mostly the new select functionality)

  • Add tests for the old fetchQuery so that it does not have new behaviour for query introduced.6

Compared to the size of the actual runtime changes and types, these were massive changes, and even though I attempted to separate changes out across multiple PRs7, the first one I landed still had the bones of 2000 lines added. So a lot of my focus was on making sure these changes were readable to Dominik. This is also why it's important if you're working on a big issue, talking about it with maintainers or other community members first helps a great deal to get people understand what you are working on.

A note about Vue

Most of our adaptors worked fine with the changes to the QueryClient as (aside from some usePrefetchQuery hooks in React, Preact and Solid), the adapters don't call the imperative methods directly and instead create and subscribe to observers and sync them to the framework8. Vue however has its own QueryClient class that proxies to the core QueryClient. This is needed to deal with Vue's reactivity system but it does mean that we had to create query and infiniteQuery on that proxy client plus tests. This part was tricky as the generic typing for fetchQuery needed to be modified for query thanks to the acceptance of enabled9.

Review and conclusion

This took some time to get over the line once I started working on it in November 2025. Mostly it was just going back and forth over 2026 with Dominik in reviews. This shouldn't surprise anyone; open-source work is mostly a part-time endeavour for the vast majority of contributors and maintainers. We finally got it over the line when Dominik took a week to do OSS work and the new methods were released in v5.102.0.

It's worth noting that the time it took to work on the PR coincided with a massive upgrade in available AI models10. This I mostly used to just copy and paste the tests for query from the old fetchQuery use cases and do code exploration. I did a few attempts to recreate a full PR from the original RFC but as of GPT-5.5 it wasn't able to land it. It been obvious over the past year that the huge amount of low quality AI pull requests are a massive headache for open-source organisations; I therefore put a strong emphasis on making sure my pull requests were thoroughly explained, that the descriptions were human generated and that I understood all the code and tests11.

Since the PR was merged, I accepted an invitation to join the TanStack maintainers group on Discord and as a collaborator on Query. I'll be hoping to help support the maintainers in fixing bugs and triaging issues. I've also have an idea to play around with the AI and Router libraries a bit more.

I'm very happy that I was able to get this PR merged. It's a small one in the grand scheme of things but it hopefully means we can explain concepts like prefetching better in our documentation and when talking to users of the library.

Hopefully this article gives you a insight in how a PR gets merged in OSS, particularly a impactful change like this one.