Deferred Data Loading
Purpose and Scope
Deferred data loading is the Router pattern for separating critical route data from slower, non-critical data. By default, TanStack Router runs loaders in parallel and waits for the awaited work to finish before rendering the next location. That default gives predictable route transitions, but it can delay visible UI when one part of the loader response is much slower than the rest. Deferred data loading keeps the awaited data path fast while allowing unresolved promises to continue in the background, so the user can see the next screen sooner and then receive the slower content when it resolves.
Sources: docs/router/guide/deferred-data-loading.md
The important design rule is that deferred data is still route data; it is just not part of the blocking portion of the transition. A route loader can await the information required to draw the meaningful shell, title, or primary record, and return an unresolved promise for secondary information such as comments, recommendations, or analytics-heavy panels. The component then reads the loader response normally and decides where to suspend. This keeps the data dependency close to the route while giving the UI a precise place to show a fallback, suspense boundary, or progressively streamed content.
Sources: docs/router/guide/deferred-data-loading.md, docs/router/api/router/awaitComponent.md
Relevant Source Files
- docs/router/guide/deferred-data-loading.md — User-facing guide for returning unresolved loader promises, rendering them with Await, and handling external data libraries differently.
- docs/router/api/router/deferFunction.md — API reference for the defer helper, including the current caution that manual wrapping is no longer usually required.
- docs/router/api/router/awaitComponent.md — API reference for the Await component, its required promise and children props, and its suspend or throw behavior.
- docs/router/api/router/useAwaitedHook.md — API reference for the useAwaited hook as the hook-shaped alternative to Await.
- examples/react/deferred-data/src/main.tsx — React example showing routes, loaders, links, a deferred comments promise, and route-level rendering around pending work.
Core Primitives
The core primitives are loaders, unresolved promises, suspense-aware readers, and route components. A loader is the route function that prepares data for a match. An unresolved promise is any promise returned from that loader response without being awaited first. The Router guide shows this by awaiting fast data, returning slow data as a promise, and allowing the route to render once the awaited data is ready. The reader side is either the Await component or the useAwaited hook, both of which suspend while pending and surface failures by throwing to the nearest error boundary.
Sources: docs/router/guide/deferred-data-loading.md, docs/router/api/router/awaitComponent.md, docs/router/api/router/useAwaitedHook.md
There is also a defer helper in the API, but the current reference explicitly cautions that applications do not need to call it manually anymore because promises are handled automatically. That distinction matters when reading older examples or existing code. The guide’s conceptual model is to return an unawaited promise, while the example application still imports and uses defer around the comments promise. Treat defer as a compatibility or state-wrapping helper that can produce a promise suitable for Await or useAwaited, not as the primary step required for every modern deferred loader.
Sources: docs/router/api/router/deferFunction.md, examples/react/deferred-data/src/main.tsx
System-to-Code Mapping
The guide maps directly to a route loader shape: fetch the slow thing, do not await it, then await the fast thing and return both values. In the example application, fetchPost starts a delayed comments request and separately awaits the post request. The returned object contains the resolved post plus commentsPromise, and that comments promise is the deferred portion of the route data. This is the same practical split the guide recommends: the post record is critical to the route, while comments can arrive after the post detail screen has begun rendering.
Sources: docs/router/guide/deferred-data-loading.md, examples/react/deferred-data/src/main.tsx
The rest of the example provides the surrounding Router context needed to understand where deferred data lives. It imports createRootRoute, createRoute, createRouter, RouterProvider, Link, MatchRoute, Outlet, Await, and TanStackRouterDevtools. The root route renders navigation and an Outlet, the posts route loads a short posts list, and the post route uses a route parameter to fetch a specific record. The visible pending spinner near MatchRoute shows that the example is not only about delayed content inside a screen, but also about navigation feedback while route work is happening.
Sources: examples/react/deferred-data/src/main.tsx
Execution Flow
A useful way to implement deferred loading is to identify what the next route must have before it can be meaningful. In a post detail page, the post itself is required; comments are useful but not required to show the page. Start both operations as early as possible, await only the critical operation, and return the slower promise alongside the resolved value. When navigation reaches the component, render the resolved value immediately, then place the promise-consuming UI in a small boundary so only the secondary region waits.
Sources: docs/router/guide/deferred-data-loading.md, examples/react/deferred-data/src/main.tsx
- Start slow, non-critical work inside the loader and keep its promise unresolved.
- Await the critical data that the route needs before it should render.
- Return an object containing both resolved values and unresolved promises.
- Read the loader result in the route component with the route’s loader-data hook.
- Render critical data immediately and pass the deferred promise to Await or useAwaited.
- Let suspense show fallback UI while pending and let the nearest error boundary handle rejection.
A simplified route follows the same pattern shown in the guide:
import { Await, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async () => {
const commentsPromise = fetchCommentsSlowly()
const post = await fetchPostQuickly()
return { post, commentsPromise }
},
component: PostComponent,
})
function PostComponent() {
const { post, commentsPromise } = Route.useLoaderData()
return (
<article>
<h1>{post.title}</h1>
<Await promise={commentsPromise} fallback={<div>Loading comments...</div>}>
{(comments) => <CommentsList comments={comments} />}
</Await>
</article>
)
}On the server, the guide describes the same pattern as streaming: the route can produce critical markup first while slower data resolves later. On the client, the user experiences this as progressive rendering with a fallback for the deferred region. The practical boundary is chosen by the component author, not by the loader. That means a single loader response can contain multiple values, and the component can decide which values are part of the primary layout and which values deserve local loading states or nested suspense boundaries.
Sources: docs/router/guide/deferred-data-loading.md, docs/router/api/router/awaitComponent.md
API Components
Await is the component API for reading a deferred promise in React 18 style code. Its required promise prop is the promise to await, and its required children prop is a render function that receives the resolved value. While the promise is pending, Await suspends by throwing the promise. If the promise rejects, Await throws the error so the nearest error boundary can render failure UI. If the promise resolves, Await calls the children function with the result. The guide also notes that React 19 users can use the platform use hook instead.
Sources: docs/router/api/router/awaitComponent.md, docs/router/guide/deferred-data-loading.md
useAwaited is the hook-shaped equivalent for code that wants the resolved value directly inside a component body. It accepts an options object with a required promise field, suspends while pending, throws if rejected, and returns the resolved value after completion. This is useful when the component’s rendering logic is easier to express after assigning the awaited value to a variable. Await is usually clearer for colocated fallback regions, while useAwaited can be convenient when the entire component section depends on the promised value.
Sources: docs/router/api/router/useAwaitedHook.md
| API | Input | Result | Failure or pending behavior |
|---|---|---|---|
| Await | promise plus children render function | Renders children with resolved data | Suspends while pending and throws rejected errors |
| useAwaited | options object with promise | Returns resolved data | Suspends while pending and throws rejected errors |
| defer | promise | Promise usable with Await or useAwaited | Manual use is no longer generally required |
External Data Libraries
Deferred loading is different when a route uses an external data library such as TanStack Query. In that approach, the route loader should kick off fetching and let the external library own caching, status, and data access through its hooks. The guide explicitly separates this from the defer and Await pattern because the library is already responsible for pending and cached state. The Router loader still matters: it is the route-aware place to start the work during navigation, but the component should read from the external library rather than treating the result as a raw deferred promise.
Sources: docs/router/guide/deferred-data-loading.md
This distinction helps avoid duplicating responsibility. Router-managed deferred promises are best when the loader itself owns the promise and the component only needs to suspend until it resolves. Query-managed data is best when cache lifetime, background refetching, and shared server state are handled elsewhere. Mixing the two without intention can make loading behavior harder to reason about, because both Router and the external library may have opinions about pending state and data availability. Choose one owner for the slow resource, then use the route loader to integrate that owner into navigation.
Sources: docs/router/guide/deferred-data-loading.md
Implementation Details and Edge Cases
Rejected deferred promises are not swallowed. Both Await and useAwaited throw when the promise rejects, which means error display belongs in the nearest applicable error boundary rather than in the loader return object itself. The example’s post request also demonstrates that critical data errors remain blocking: a missing post is handled during the awaited post fetch, before the route can treat the comments as a secondary concern. This separation is important for user experience, because a page should not render a detail layout for a primary record that failed to load.
Sources: docs/router/api/router/awaitComponent.md, docs/router/api/router/useAwaitedHook.md, examples/react/deferred-data/src/main.tsx
Code splitting changes how a component may access typed loader data, but it does not change the deferred data model. The guide notes that a code-split component can use getRouteApi to avoid importing the route configuration solely to access the typed useLoaderData hook. That is a composition concern rather than a loading concern. The loader still returns resolved and unresolved values, the component still consumes those values, and the suspense or error boundary still determines what the user sees while deferred work is pending or rejected.
Sources: docs/router/guide/deferred-data-loading.md
Related Pages and Next Steps
After implementing deferred data loading, read the broader Data Loading page to understand loader dependencies, caching, invalidation, stale times, and pending states. Then read External Data Loading and Mutations if TanStack Query or another cache owns the slow resource. For production server-rendered applications, pair this page with Server-Side Rendering because the guide identifies streaming as the server-side version of the same progressive rendering pattern. If route modules are split into separate files, continue to Code Splitting so deferred promises remain typed without forcing unnecessary route imports.
Sources: docs/router/guide/deferred-data-loading.md