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 user something sooner while the rest of the data loads in the background.
Deferred data loading is a pattern that allows the router to render the next location's critical data/markup while slower, non-critical route data is resolved in the background. This process works on both the client and server (via streaming) and is a great way to improve the perceived performance of your application.
If you are using a library like TanStack Query or any other data fetching library, then deferred data loading works a bit differently. Skip ahead to the Deferred Data Loading with External Libraries section for more information.
To defer slow or non-critical data, return an unawaited/unresolved promise anywhere in your loader response:
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async () => {
// Fetch some slower data, but do not await it
const slowDataPromise = fetchSlowData()
// Fetch and await some data that resolves quickly
const fastData = await fetchFastData()
return {
fastData,
deferredSlowData: slowDataPromise,
}
},
})
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async () => {
// Fetch some slower data, but do not await it
const slowDataPromise = fetchSlowData()
// Fetch and await some data that resolves quickly
const fastData = await fetchFastData()
return {
fastData,
deferredSlowData: slowDataPromise,
}
},
})
As soon as any awaited promises are resolved, the next route will begin rendering while the deferred promises continue to resolve.
In the component, deferred promises can be resolved and utilized using the Await component:
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute, Await } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})
function PostIdComponent() {
const { deferredSlowData, fastData } = Route.useLoaderData()
// do something with fastData
return (
<Await promise={deferredSlowData} fallback={<div>Loading...</div>}>
{(data) => {
return <div>{data}</div>
}}
</Await>
)
}
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute, Await } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})
function PostIdComponent() {
const { deferredSlowData, fastData } = Route.useLoaderData()
// do something with fastData
return (
<Await promise={deferredSlowData} fallback={<div>Loading...</div>}>
{(data) => {
return <div>{data}</div>
}}
</Await>
)
}
Tip
If your component is code-split, you can use the getRouteApi function to avoid having to import the Route configuration to get access to the typed useLoaderData() hook.
The Await component resolves the promise by triggering the nearest suspense boundary until it is resolved, after which it renders the component's children as a function with the resolved data.
If the promise is rejected, the Await component will throw the serialized error, which can be caught by the nearest error boundary.
Tip
In React 19, you can use the use() hook instead of Await
When your strategy for fetching information for the route relies on External Data Loading with an external library like TanStack Query, deferred data loading works a bit differently, as the library handles the data fetching and caching for you outside of TanStack Router.
So, instead of using defer and Await, you'll instead want to use the Route's loader to kick off the data fetching and then use the library's hooks to access the data in your components.
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
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())
},
})
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
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 your component, you can use the library's hooks to access the data:
// src/routes/posts.$postId.tsx
import * as React from 'react'
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())
// do something with fastData
return (
<React.Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</React.Suspense>
)
}
function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())
return <div>{data}</div>
}
// src/routes/posts.$postId.tsx
import * as React from 'react'
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())
// do something with fastData
return (
<React.Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</React.Suspense>
)
}
function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())
return <div>{data}</div>
}
Streamed promises follow the same lifecycle as the loader data they are associated with. They can even be preloaded!
Streaming requires a server that supports it and for TanStack Router to be configured to use it properly.
Please read the entire SSR Guide for step by step instructions on how to set up your server for streaming.
The following is a high-level overview of how deferred data streaming works with TanStack Router:
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.