T
TanStack5mo ago
variable-lime

Tanstack Start, React Query, deferred loading, prefetch, streaming

I have been trying to achieve deferred loading for hours now and I cannot make it work. Basically I have /dashboard/* routes that are only accessible to authenticated users. So in my dashboard/route.tsx layout, I prefetch in the loader without awaiting and useSuspenseQuery in the layout. Even this simple setup doesn't work. When I look at the network tab I can see a client call the /me trpc
export const Route = createFileRoute('/dashboard')({
component: RouteComponent,
loader: async ({ context }) => {
context.queryClient.prefetchQuery({
...context.trpc.me.queryOptions(), // this query takes 4 seconds
staleTime: Infinity,
})
},
})

function RouteComponent() {
const trpc = useTRPC()
const { data: me } = useSuspenseQuery({
...trpc.me.queryOptions(),
staleTime: Infinity,
})

return <div>{me.email}</div>
}
export const Route = createFileRoute('/dashboard')({
component: RouteComponent,
loader: async ({ context }) => {
context.queryClient.prefetchQuery({
...context.trpc.me.queryOptions(), // this query takes 4 seconds
staleTime: Infinity,
})
},
})

function RouteComponent() {
const trpc = useTRPC()
const { data: me } = useSuspenseQuery({
...trpc.me.queryOptions(),
staleTime: Infinity,
})

return <div>{me.email}</div>
}
What I am trying to achieve is showing a loading while the query resolves. The documentation mention NextJS but not Tanstack Start. For example, on some NextJS documentation they say you need shouldDehydrateQuery. Do I need it for Tanstack Start ?
const queryClient = new QueryClient({
defaultOptions: {
dehydrate: {
serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrate: { deserializeData: superjson.deserialize },
queries: {
retry: false,
},
},
})
const queryClient = new QueryClient({
defaultOptions: {
dehydrate: {
serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrate: { deserializeData: superjson.deserialize },
queries: {
retry: false,
},
},
})
21 Replies
passive-yellow
passive-yellow5mo ago
take a look at this example https://github.com/TanStack/router/blob/af455d812f220a6df3d43dbe1610cf0d173ea092/e2e/react-start/basic-react-query/src/router.tsx the react query plugin handles configuring the hydration / dehydration parts
GitHub
router/e2e/react-start/basic-react-query/src/router.tsx at af455d81...
🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering. - TanStack/router
passive-yellow
passive-yellow5mo ago
also just a note that loaders run on the client as well so when its working you shouldn't see the network call only for the initial page load
variable-lime
variable-limeOP5mo ago
Thanks ! This is so confusing, there is a lot of examples online and they are all different for example, your link shows const queryClient = new QueryClient()inside of createRouter. Isn't createRouter only called once when the server starts ? Does that mean every requests will share the same query client ? On the NextJS doc they say you need to create a new query client every request to avoid sharing cache between users
passive-yellow
passive-yellow5mo ago
I'm not an expert so take my input with skepticism but createRouter gets called on the server during SSR for each incoming request and also on the client. So the life of the context and query client on the server is just the length of the request and it moves to the client where the router and query cache are hydrated from the server. I was also a little bit confused at first as there's a lot of documentation around the topic with subtle differences depending on the framework.
variable-lime
variable-limeOP5mo ago
Should I see a network request immediately after the page loads ? I just double-checked and you are right, createRouter gets called on every requests
passive-yellow
passive-yellow5mo ago
It depends on whether or not you await the fetch / prefetch and if it's a server load or client transition so if you await a prefetch in a route and you load that page directly, you shouldn't see the network call but if you navigate to it from another page you will since the loader will run on the client in that case maybe the await doesn't matter honestly it's hard to keep track of all these things
variable-lime
variable-limeOP5mo ago
My query takes 4 seconds to resolve. I don't await the prefetchQuery. The page takes 4 seconds to load. I see another client request after the page loads that takes another 4 seconds
passive-yellow
passive-yellow5mo ago
I think it shouldn't matter because of the streaming the fetch should still happen on the server if the loader ran on the server
variable-lime
variable-limeOP5mo ago
yes it's loading fine but it not very efficient I tried staleTime: Infinity but still, the client refetch
passive-yellow
passive-yellow5mo ago
I'll share some relevant parts of my app where it's working as expected (confirmed await doesn't matter since it streams)
variable-lime
variable-limeOP5mo ago
Ok thank you for your help So, I had a defaultoptions StaleTime: 0 in the createRouter and staleTime: Infinity in my layout. I thought it would override the default but apparently not so now it's working, the client doesn't refetch on load
passive-yellow
passive-yellow5mo ago
So in this case if I load the home page directly ie refresh I don't see a network call to the upcoming meetings query
passive-yellow
passive-yellow5mo ago
Oh nice. From what I could tell staleTime on the server didn't seem to have an effect on the query when it made it to the client
variable-lime
variable-limeOP5mo ago
ohhh they don't share default options maybe that's why
passive-yellow
passive-yellow5mo ago
I don't set any staletime on my query client or server query though and it is working as expected I'm loving the developer experience with the router and query. Super nice to be able to drill down into all your data fetching and routing stuff
variable-lime
variable-limeOP5mo ago
Yes it's really cool So now that my setup is working, time to experiement with loader. I read that if you await prefetch, the page load pauses until the prefetch is done. And if you don't await prefetch, you can show a loader while the promise resolves with <Suspense> or <Await> I'll try
passive-yellow
passive-yellow5mo ago
Yep that should be how it works Good luck !
variable-lime
variable-limeOP5mo ago
Thanks so much for your help man
variable-lime
variable-limeOP5mo ago
Ok, awaiting or not the prefetchquery doesn't change anything, the page pauses until the loader is finished. The example here https://tanstack.com/router/latest/docs/framework/react/guide/deferred-data-loading#deferred-data-loading-with-external-libraries doesn't work for me
Deferred Data Loading | TanStack Router React Docs
TanStack Router is designed to run loaders in parallel and wait for all of them to resolve before rendering the next route. This is great most of the time, but occasionally, you may want to show the u...
variable-lime
variable-limeOP5mo ago
Not awaiting prefetch and wrapping in suspense doesn't show the fallback
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})

function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions())

// do something with fastData

return (
<Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</Suspense>
)
}

function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())

return <div>{data}</div>
}
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})

function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions())

// do something with fastData

return (
<Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</Suspense>
)
}

function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())

return <div>{data}</div>
}
Maybe because I am in a layout route (dashboard.route.tsx) and not in a page route...
passive-yellow
passive-yellow5mo ago
Weird, I don't think that should matter tbh

Did you find this page helpful?