ContextSometimes, 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 similar, and it also makes sense for that use case. And as time goes on, we might do this a bunch of times, and in isolation, each little interaction made sense on its own. But if we take a step back and look at the big picture, we might have inadvertently created something that isn’t nice to work with. It might make sense to someone who knows the “historical reasons”, but for someone coming in with a fresh set of eyes, it might look weird. The imperative methods on the HistoryAt first, we needed a function to imperatively fetch a query, so we created Then, And then finally, route loaders become popular, so we needed a way to integrate with those, too. Throwing errors is usually what you want to integrate with error boundaries, but we didn’t really want to “wait” in the route loaders if data was already present, because advancing to the component with stale data is usually fine, as they’ll trigger a background refetch anyways. That’s why we added Now all these steps made sense in isolation, but when you look at this, we now have 3 APIs that are pretty close in functionality. Actually, it’s 6 APIs because we need the same set of functions for infinite queries:
Problem DescriptionNow that we have those APIs, we can see a bit of confusion around them as well: Confusion around naming
Confusion around when to use whatFor route loaders, we recommend Further, using Current APIsLet’s again look at the three APIs, what they do and how they differ from each other:
So, it’s undeniable that they are very similar, and the distinction by use-case isn’t really helpful, as the user needs to decide very early which case they want. Proposed SolutionThe power of So, we want the same for our imperative APIs, which is why we want to move towards: queryClient.query(options)
queryClient.infintiteQuery(options)Per default, this should behave like
Migration Path
|
Replies: 23 comments 46 replies
|
On a related note, would it also be worth exploring a similar unification for the
They each serve a distinct purpose, but from a DX perspective, it’s starting to feel like the surface area is expanding similarly to the imperative methods you’re aiming to clean up. Could we consider an approach where these behaviours are driven more by options rather than separate exports? For instance, This could make adoption and migration simpler, especially for teams navigating multiple query behaviours. Curious to hear others' thoughts—are there any strong reasons why keeping them separate is more beneficial? |
|
The only thing I can say for this RFC / new API proposal is: Beautiful |
|
why await queryClient.query(options, { throwOnError: false })over? await queryClient.query(options).catch(noop)so that onError callbacks in |
|
I approve this message. |
This comment has been hidden.
This comment has been hidden.
|
I like this a lot. It was indeed confusing to me what the difference was between ensure/prefetch, nobody in the Discord could give a clear answer and even the docs didn’t really clarify if (let alone why) you should use one over the other in the context of a route loader. Your explanation above is already way better than what’s in the docs currently, I suggest adding it until this RFC is implemented. |
|
If these can be consolidated I think it would be a big win. I get confused between ensureQueryData and prefetchQuery all the time. |
|
I'm a heavy React Query user, and I still get confused by all the imperative methods it exposes to fetch, refetch, or invalidate data. These changes are very welcome. I much prefer: await queryClient.query(options).catch(noop) over: await queryClient.query(options, { throwOnError: false }) I don't like dictating the control flow or types through props. This solution should stay in JS land, so it's intuitive for users to handle these cases the way they normally do. JS land solutions are more intuitive than relying on command spacing to check if a property exists in a specific argument position or searching through docs to check if there is a property that solves my problem. By JS land, I mean: if you know JavaScript, you know how to handle this. What if I want to trigger a toast on error without interrupting the call stack? I'd have to opt out of one API and switch to another just for that use case. The Expanding the props pattern can lead to monstrosities like I love that you separated the |
|
What's difference between imperative api for query vs |
|
Think this makes a lot of sense and should be easier to explain these concepts in questions about router loaders and the like. I've been explaining the differences myself not necessarily off any docs but this talk Tanner did here on critical data (https://youtube.com/clip/UgkxNlG5s7DDYRnilAg0sV3KLFWUgrswtFDM). I guess the new docs for route loaders is use |
|
I like this proposal a lot. I've looked up which of these is the right one sooo many times. Making the functionality explicit seems like the right move. |
|
Small update: Not sure why I thought we need a That’s also consistent with how we allow e.g. I’ve updated the RFC accordingly. |
|
I like the idea of making the API more streamlined. The reason why I have implemented this was because I thought that While we are at it and I know you (@TkDodo) are against this. |
|
Hi @TkDodo is there any progress on this? How can I help to get this feature done, I'd like to help |
|
Question not directly related to the discussion here (although I guess, made moot by its implementation).
That does not match the documentation. https://tanstack.com/query/v5/docs/framework/react/guides/prefetching#router-integration uses I guess with just |
|
Is it still the case that'd you'd want this done in steps? I've given implementing this a shot in a branch on my fork and replacing the usages of the old methods in tests are a lot. |
|
Now that we mutationOptions, I would love an imperative way to run mutations as well. I've been using useMutation+mutationOptions to coordinate the cache updating. I'm looking at executing those same mutationOptions in a tanstack/start loader. It would be nice to be able to have a nicer, imperative ui: queryClient.mutate(myMutationOptions, data);
// or (not as good)
queryClient.createMutation(myMutationOptions).exec(data);Right now I'm doing this, which seems to work but is verbose. context.queryClient
.getMutationCache()
.build(context.queryClient, userStateMutationOptions)
.execute({ data: { ... } }); |
|
The direction of this RFC is really compelling. The historical accretion of The proposed The The composition pattern for the export async function loader({ params }) {
// Serve immediately from cache if available, regardless of staleness
const data = await queryClient.query({
...userQuery(params.id),
staleTime: 'static',
});
// Then kick off a background refresh if the data is actually stale
void queryClient.query(userQuery(params.id));
return data;
}This is more code than One question for the team: will |
|
Status update: Merged
Approved
Open
|
|
Man, this is so frustrating. I'm torturing Fable for hours to understand what's the difference and when to use what and still don't get it. Additionally reading articles like TanStack Router and Query which has:
Like WHY? Why it doesn't matter, why there are 3 options. Moreover there's |
|
All PR's to implement this have been merged into main: When the release ships, #10662 will update the docs |
|
This RFC is now shipped in v5.102.0 https://github.com/TanStack/query/releases/tag/release-2026-08-22-1856 |
|
Just wondering what the recommend approach is for using this inside loaders in tanstack start? Do we just use For some background, we are trying to kick off loading immediately in a loader and then in the route component we Example |
This RFC is now shipped in v5.102.0
https://github.com/TanStack/query/releases/tag/release-2026-08-22-1856