T
TanStack•8mo ago
automatic-azure

ensure vs prefetch query in deferred data loading

I'm trying to understand the difference between ensureQueryData and prefetchQueryData, esepcially in the context of deferred loading. From the tanstack router docs:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())

// Fetch and await some data that resolves quickly
await queryClient.ensureQueryData(fastDataOptions())
},
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())

// Fetch and await some data that resolves quickly
await queryClient.ensureQueryData(fastDataOptions())
},
})
Then in the component:
function PostIdComponent() {
const slowData = useSuspenseQuery(slowDataOptions())
return (
<Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</Suspense>
)
}
function PostIdComponent() {
const slowData = useSuspenseQuery(slowDataOptions())
return (
<Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</Suspense>
)
}
I get why we don't await the prefetch query, since we want to let it resolve in a <Suspense /> block. But from the docs it seems like the main diff. between the two is that prefetch will never throw, and ensureQueryData will prefer to serve cached data. But for instance, why can't both use prefetch or both use ensure?
16 Replies
conscious-sapphire
conscious-sapphire•8mo ago
automatic-azure
automatic-azureOP•8mo ago
Super helpful, thank you!
grumpy-cyan
grumpy-cyan•8mo ago
FYI this question comes up so often that I'm thinking about depreciating all the methods and just do queryClient.query(...) with some options. The difference are so minor that it usually doesn't matter Also, when you use suspense, there is really no reason to await anything in the route loaders. Just kick off all things in parallel and let the components render and suspend if data isn't ready yet
conscious-sapphire
conscious-sapphire•8mo ago
You mean the child suspense boundaries, not the auto wrapped suspense at route level?
grumpy-cyan
grumpy-cyan•8mo ago
I mean all of them Even if you don't add your own suspense boundary, router would render the component and then useSuspenseQuery would just immediately throw to the router suspense boundary again
conscious-sapphire
conscious-sapphire•8mo ago
So I guess by that definition "critical data" is just something you suspend on immediately, and "non critical" data is stuff with its own sub boundary and loading fallback
grumpy-cyan
grumpy-cyan•8mo ago
why woudl we even need to differentiate between the two when using react-query? the code shown above won't even work because this:
const slowData = useSuspenseQuery(slowDataOptions())
const slowData = useSuspenseQuery(slowDataOptions())
is outside the Suspense bounary so it will go to the router suspense boundary anyway ... I would see it like that:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
void queryClient.ensureQueryData(slowDataOptions())

// Fetch some data that resolves quickly but also don't await it
void queryClient.ensureQueryData(fastDataOptions())
},
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
void queryClient.ensureQueryData(slowDataOptions())

// Fetch some data that resolves quickly but also don't await it
void queryClient.ensureQueryData(fastDataOptions())
},
})
and then in the component:
function PostIdComponent() {
// this will suspend to the router boundary
const criticalData = useSuspenseQuery(fastDataOptions())
return (
<Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</Suspense>
)
}

function SlowDataComponent() {
// this will suspend to the manually built suspense boundary from the parent
const slowData = useSuspenseQuery(slowDataOptions())
}
function PostIdComponent() {
// this will suspend to the router boundary
const criticalData = useSuspenseQuery(fastDataOptions())
return (
<Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</Suspense>
)
}

function SlowDataComponent() {
// this will suspend to the manually built suspense boundary from the parent
const slowData = useSuspenseQuery(slowDataOptions())
}
the thing is: once you kick off fetching in the route loader, it's all good. Suspense won't waterfall anymore. You can then compose useSuspenseQuery however you want. If you want to wait for everything to reveal at the same time, just do:
function PostIdComponent() {
const criticalData = useSuspenseQuery(fastDataOptions())
const slowData = useSuspenseQuery(slowDataOptions())
}
function PostIdComponent() {
const criticalData = useSuspenseQuery(fastDataOptions())
const slowData = useSuspenseQuery(slowDataOptions())
}
that's the same as awaiting them both in the route loader - because they'll both hit the router suspense boundary. Or am I wrong 😅 ? @Manuel Schiller tell me I'm wrong like, what's the point in awaiting anything in a route loader if we don't intend to ever useLoaderData ... I guess maybe pendingMs and pendingMinMsdon't come into play then? Because the loaders are basically synchronous if you don't await anything in them 🤔
useful-bronze
useful-bronze•8mo ago
there are cases in Start (or any SSR setup) where you would want to await in the loader recently saw one example where the SSR response needed to contain cookie header that was set by calling a server function using query if you do not await the loader, that cookie will not make it through to the client
grumpy-cyan
grumpy-cyan•8mo ago
that was set by calling a server function using query
hmm, queries shouldn't have such side-effects ?
useful-bronze
useful-bronze•8mo ago
well sometimes they do ... when you call a server function and it sets a header, then the SSR response will inherit that header but only if you wait for it
grumpy-cyan
grumpy-cyan•8mo ago
okay but for pure non-SSR situations?
useful-bronze
useful-bronze•8mo ago
there are other examples, e.g. reading the query result in e.g. meta(), also needs to happen before the response is built i can only think of SSR situations right now
grumpy-cyan
grumpy-cyan•8mo ago
yeah sure, if you need the result somewhere, you need to await; same if you want to prefetch conditional queries
useful-bronze
useful-bronze•8mo ago
but TBH my head is quite SSR right now. it totally shifted from SPA to SSR in the last months 😄 not because I prefer SSR just because of working on it ...
grumpy-cyan
grumpy-cyan•8mo ago
just to add to that: I thought that if you trigger a prefetch on the server without awaiting, the component would render on the server and the useSuspenseQueryCall there would then also suspend on the server . So why does awaiting in the loader matter? also this seems magical ... too magical maybe ? If setting / forwarding the header were explicit, you'd know that you have to await it...
useful-bronze
useful-bronze•8mo ago
you can only set a response cookie before you finished sending the headers. but useSuspenseQuery (and thus the "response" of the server function) kicks in after the headers were already sent off yes, I agree. setting the header inside the server function is already explicit, but the receiving part is not explicitly forwarding

Did you find this page helpful?