Replies: 7 comments 4 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? |
Beta Was this translation helpful? Give feedback.
-
The only thing I can say for this RFC / new API proposal is: Beautiful |
Beta Was this translation helpful? Give feedback.
-
why await queryClient.query(options, { throwOnError: false }) over? await queryClient.query(options).catch(noop) so that onError callbacks in |
Beta Was this translation helpful? Give feedback.
-
I approve this message. |
Beta Was this translation helpful? Give feedback.
-
Says “refetchQuery” instead of prefetch. Typo? |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
If these can be consolidated I think it would be a big win. I get confused between ensureQueryData and prefetchQuery all the time. |
Beta Was this translation helpful? Give feedback.
-
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 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
queryClient
are such an example.History
At first, we needed a function to imperatively fetch a query, so we created
queryClient.fetchQuery(options)
. This function respects caching andstaleTime
, so it will only fire a fetch if there is no fresh data in the cache, it returns a Promise that can be awaited etc. It’s not really used for displaying data in a component, but rather to combine it with things like async validations.Then,
prefetching
became a thing, to e.g. fetch data when you hover a link, so that you can hopefully get the result before the user sees a loading indicator. We didn’t really wantdata
to be returned, and we definitely didn’t want errors to be thrown, so we created a new methodqueryClient.prefetchQuery(options)
, which usesfetchQuery
under the hood, but is optimized for this use-case.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
ensureQueryData
, which is also to some extent built onfetchQuery
. To make things worse, we even added an option toensureQueryData
to trigger background refetches (invalidateIfStale
), which is awfully close to just callingfetchQuery
without awaiting the promise.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:
queryClient.fetchQuery
queryClient.fetchInfiniteQuery
queryClient.prefetchQuery
queryClient.prefetchInfiniteQuery
queryClient.ensureQueryData
queryclient.ensureInfiniteQueryData
Problem Description
Now that we have those APIs, we can see a bit of confusion around them as well:
Confusion around naming
queryClient.fetchQuery
, despite the name, might not invoke thequeryFn
. If data in the cache is fresh, it will just give you that. Yes, that’s whatuseQuery
does as well, but it’s not really reflected in the naming.queryClient.prefetchQuery
has a similar naming problem: thepre
inprefetchQuery
indicates that something is done once, before it’s needed / available. But did you know that callingprefetchQuery
will also fetch every time when data is stale? So this code:will not only put data in the cache when you hover it for the first time - it will do it on every hover interaction (given that
staleTime
has the default ofzero
).Confusion around when to use what
For route loaders, we recommend
ensureQueryData
. In the SSR docs, we recommendprefetchQuery
. The functions are so close in functionality that it mostly doesn’t matter, so why is it two functions? We get so many questions around “what should I use where?”, which is a good indicator that the APIs are not intuitive.Further, using
prefetchQuery
during SSR means that the error boundary won’t be invoked because it doesn’t throw errors. That might not matter much for server components, as errors on the server will trigger aSuspense
boundary and an automatic retry on the client, but it might matter for route loaders if you want to show the errors immediately.Current APIs
Let’s again look at the three APIs, what they do and how they differ from each other:
queryClient.fetchQuery({ ... options, staleTime })
queryFn
unless data is already in the cache that is considered fresh (determined by the passedstaleTime
)Promise<TData>
that can be awaited (might resolve immediately for fresh data).queryClient.prefetchQuery({ ... options, staleTime })
queryFn
unless data is already in the cache that is considered fresh (determined by the passedstaleTime
)Promise<void>
that can be awaited (might resolve immediately for fresh data).fetchQuery(options).then(noop).catch(noop)
queryClient.ensureQueryData({ ... options, staleTime, revalidateIfStale })
queryFn
only if NO data is in the cache, so it doesn’t check forstaleTime
Promise<TData>
that can be awaited (might resolve immediately for fresh data).staleTime
to trigger a background refetch whenrevalidateIfStale
is passed. This is meant to immediately return data and update the cache as early as possible.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 Solution
The power of
useQuery
comes from the fact that you have one function that you can just use with defaults and it will work as you’d expect for many cases. Then, it allows for some customization options to handle different cases on an opt-in basis. We used to have different hooks for pagination (usePaginatedQuery
) but quickly moved away from that because of similar reasons: the distinction didn’t really matter.So, we want the same for our imperative APIs, which is why we want to move towards:
Per default, this should behave like
queryClient.fetchQuery
does today:staleTime
(like any goodquery
should)await
.Migration Path
queryClient.prefetchQuery
This function will become:
throwOnError: false
, you can re-create the part where errors aren’t thrown. This option also exists onuseQuery
and other imperative methods that target multiple queries likequeryClient.refetchQueries
.onMouseEnter
example from before would work fine even without changingthrowOnError
, as the promise get’s ignored withvoid
explicitly. Thus, no unhandled promise rejection happens.fetchOptions
(like we have inrefetchQueries
or if this should just be merged withoptions
.void
orawait
.queryClient.ensureQueryData
This function will become:
symbol
,StaleTime.Static
that will act as an indicator to mark a query as, well,static
. Static queries will never be revalidated when any data exists in the cache. The difference tostaleTime: Infinity
is thatInfinity
is still just a number, which means queries that are invalidated withqueryClient.invalidateQueries
would get refetched, even if they have an infinitestaleTime
(withStatic
, this is not the case). This was one of the main reasons to introduceensureQueryData
, butstaleTime: StaleTime.Static
solves this problem betterStaleTime
can also be used anywhere else wherestaleTime
is passed, e.g. onuseQuery
, and it would there too stop a query to be refetched even if it gets marked asinvalid
.Caveats
One reason why we recommend
prefetchQuery
in server components is the fact that it doesn’t return anything, so users can’t make the mistake of using the returned data in it. The problem you might run into when doing that is that it can get out-of-sync when a revalidation happens on the client only. There’s a great example in the [Advanced Server Rending section of the docs](https://tanstack.com/query/v5/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) about this.With the new method, it would be on you to either
await
the data, but not use it:or to simply not await it and [stream the promise to the client](https://tanstack.com/query/v5/docs/framework/react/guides/advanced-ssr#streaming-with-server-components):
which is likely the better approach anyways.
What about
revaliateIfStale
onensureQueryData
?Right now, I don’t think it was a good idea to introduce this functionality in the first place, so we’re not going to re-create it. If you want to read data imperatively (or fetch it if it doesn’t exist), and refetch it as well in the background if it exists but is stale, you can make two calls to
queryClient.query
:Rollout strategy
We plan to add the new functions in a v5 minor and mark the existing functions as
deprecated
. We’ll then likely remove them in the next major version.Beta Was this translation helpful? Give feedback.
All reactions