#ensure vs prefetch query in deferred data loading

33 messages · Page 1 of 1 (latest)

white tendon
#

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())
  },
})

Then in the component:

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?

gusty pollen
#

This is the best explanation of the differences so far #query message

white tendon
#

Super helpful, thank you!

kindred steeple
#

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

gusty pollen
#

You mean the child suspense boundaries, not the auto wrapped suspense at route level?

kindred steeple
#

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

gusty pollen
#

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

kindred steeple
#

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())

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())
  },
})

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())
}
#

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())
}

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 😅 ?

#

@fleet tartan 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 🤔

fleet tartan
#

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

kindred steeple
#

that was set by calling a server function using query
hmm, queries shouldn't have such side-effects ?

fleet tartan
#

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

kindred steeple
#

okay but for pure non-SSR situations?

fleet tartan
#

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

kindred steeple
#

yeah sure, if you need the result somewhere, you need to await; same if you want to prefetch conditional queries

fleet tartan
#

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 ...

kindred steeple
kindred steeple
fleet tartan