T
TanStack4mo ago
fascinating-indigo

Suspense around Outlets

I am using Apollo Client with Tanstack Router. I have wrapped an <Outlet /> in a <Suspense> and I am using useReadQuery to receive preloaded data from the loader I would assume this would trigger the Suspense fallback based on this line from the Apollo documenation The preload function returns a queryRef that is passed to useReadQuery to read the query data and suspend the component while loading. Here is my example code route.tsx
export const Route = createFileRoute('/_auth/priority')({
component: Priority,
});

function Priority() {
return (
<div className="flex h-full gap-2">
<PriorityList className="h-full" /> <-- These list items '<Link>' to the '/priority/$dealId' routes

...

<div className="flex-1">
<Suspense fallback={<div>Loading... from priority route</div>}>
<Outlet /> <-- Display list item details
</Suspense>
</div>
</div>
);
}
export const Route = createFileRoute('/_auth/priority')({
component: Priority,
});

function Priority() {
return (
<div className="flex h-full gap-2">
<PriorityList className="h-full" /> <-- These list items '<Link>' to the '/priority/$dealId' routes

...

<div className="flex-1">
<Suspense fallback={<div>Loading... from priority route</div>}>
<Outlet /> <-- Display list item details
</Suspense>
</div>
</div>
);
}
$dealId.tsx
export const Route = createFileRoute('/_auth/priority/$dealId')({
loader: ({ params }) =>
preloadQuery(DealQuery, {
returnPartialData: true,
variables: { dealId: params.dealId },
}),
component: RouteComponent,
});

function RouteComponent() {
const dealQueryRef = Route.useLoaderData();
const { data } = useReadQuery(dealQueryRef);

return <DealInfo />;
}
export const Route = createFileRoute('/_auth/priority/$dealId')({
loader: ({ params }) =>
preloadQuery(DealQuery, {
returnPartialData: true,
variables: { dealId: params.dealId },
}),
component: RouteComponent,
});

function RouteComponent() {
const dealQueryRef = Route.useLoaderData();
const { data } = useReadQuery(dealQueryRef);

return <DealInfo />;
}
This layout does not show my Suspense fallback in the route.tsx file. However when I pass down the dealQueryRef to the <DealInfo /> component, wrap it in a <Suspense> and call useReadQuery from inside the fallback shows up correctly in the $dealId.tsx route. Perhaps @jerelmiller might be of assistance as well.
Apollo GraphQL Docs
Suspense
5 Replies
fascinating-indigo
fascinating-indigoOP4mo ago
Based on the example from Deferred Data Loading
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
component: PostIdComponent,
})

function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions()) <-- suspend the route component

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

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

return <div>{data}</div>
}
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
component: PostIdComponent,
})

function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions()) <-- suspend the route component

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

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

return <div>{data}</div>
}
I thought just wrapping the <Outlet /> in the route.tsx would suffice when the useReadQuery suspended the Route component pendingComponent isn't showing anything as well This does show something as explained in the docs. When I put pendingComponent in my $dealId.tsx route it works as expected. Is pendingComponent supposed to act as the <Suspense> around an <Outlet /> and we are not supposed to wrap <Outlet />s?
ugly-tan
ugly-tan4mo ago
yes if you use useSuspenseQuery, the pending component will be rendered if data is loading. But keep in mind, that it will only show, when data takes longer as the default minimum time defined in your router.
defaultPreloadDelay: 50
defaultPendingMs: 1000
defaultPendingMinMs: 500
defaultPreloadDelay: 50
defaultPendingMs: 1000
defaultPendingMinMs: 500
those are the defaults. So if you query takes longer than 500ms the pending component will be shown for 1000ms.
continuing-cyan
continuing-cyan3mo ago
I'm facing the same issue with Tanstack Query. - Wrapping <Outlet /> with <Suspense /> never renders the fallback component. - Wrapping the child component directly inside of <Suspense /> works fine.
// ✅ This renders the fallback component:

export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
},
component: Posts,
})

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

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

return <div>{data}</div>
}

// ⛔ This won't render the fallback component and will await the query:

// ./router.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
},
component: Posts,
})

function Posts() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Outlet />
</Suspense>
)
}

// ./index.tsx
export const Route = createFileRoute('/posts/')({
component: SlowDataComponent,
})

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

return <div>{data}</div>
}
// ✅ This renders the fallback component:

export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
},
component: Posts,
})

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

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

return <div>{data}</div>
}

// ⛔ This won't render the fallback component and will await the query:

// ./router.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
},
component: Posts,
})

function Posts() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Outlet />
</Suspense>
)
}

// ./index.tsx
export const Route = createFileRoute('/posts/')({
component: SlowDataComponent,
})

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

return <div>{data}</div>
}
I might be missing something but it feels like it should work either way, regardless of defaultPendingMs, defaultPendingMinMs or defaultPreloadDelay.
ugly-tan
ugly-tan3mo ago
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
},
component: Posts,
pendingComponent: YourPendingComponent || <div>Loading...</div>
})

function Posts() {
return (
<Outlet />
)
}

// ./index.tsx
export const Route = createFileRoute('/posts/')({
component: SlowDataComponent,
})

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

return <div>{data}</div>
}
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())
},
component: Posts,
pendingComponent: YourPendingComponent || <div>Loading...</div>
})

function Posts() {
return (
<Outlet />
)
}

// ./index.tsx
export const Route = createFileRoute('/posts/')({
component: SlowDataComponent,
})

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

return <div>{data}</div>
}
did you try this setup? I think that is how you should be implementing it. Outlets are already wrapped with Suspense.
continuing-cyan
continuing-cyan3mo ago
After further testing with a fake fetch with 3 seconds delay and lightweight rendering on the arrival page I can confirm that everything works as expected. My api call resolving fast enough and my arrival page heavy rendering in dev mode alongside defaultPendingMs might have been the culprits. I won't do further testing as I no longer need a fix.

Did you find this page helpful?