Capítulo 9 de 24
A single-file, code-based routing example (src/main.tsx) showing how to return a mix of awaited and deferred values from a route loader: fetch the primary data (a post) up front, but wrap a slower secondary fetch (its comments) in defer() so the route renders immediately and streams the comments in once they resolve, with an explicit <React.Suspense> + <Await> boundary in the component.
createRootRoute / createRoute / .addChildren, no routeTree.gen)@tanstack/react-router, @tanstack/react-router-devtools, redaxios, zod (present in package.json though this file doesn't validate search)src/main.tsx: fetch helpers, root/index/posts/post routes, the deferred-loader logic, and the RouterProvider bootstrap.const fetchPost = async (postId: string) => {
const commentsPromise = new Promise((r) => setTimeout(r, 2000))
.then(() =>
axios.get<Array<CommentType>>(
`https://jsonplaceholder.typicode.com/comments?postId=${postId}`,
),
)
.then((r) => r.data)
const post = await new Promise((r) => setTimeout(r, 1000))
.then(() => axios.get<PostType>(`https://jsonplaceholder.typicode.com/posts/${postId}`))
.catch((err) => {
if (err.status === 404) throw new NotFoundError(`Post with id "${postId}" not found!`)
throw err
})
.then((r) => r.data)
return {
post,
commentsPromise: defer(commentsPromise),
}
}
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
loader: async ({ params: { postId } }) => fetchPost(postId),
errorComponent: PostErrorComponent,
component: PostComponent,
})
function PostComponent() {
const { post, commentsPromise } = postRoute.useLoaderData()
return (
<div className="space-y-2">
<h4 className="text-xl font-bold underline">{post.title}</h4>
<div className="text-sm">{post.body}</div>
<React.Suspense fallback={<div>Loading comments...</div>} key={post.id}>
<Await promise={commentsPromise}>
{(comments) => (
<div className="space-y-2">
{comments.map((c) => <div key={c.id}>{c.name}</div>)}
</div>
)}
</Await>
</React.Suspense>
</div>
)
}
defer() marks a promise as non-blocking for the loader, and <Await> inside a <Suspense> boundary resolves it in the component tree, with the key={post.id} on Suspense resetting the boundary when the id param changes.defer(); keep anything needed for the initial render (post) as a plain await, so the route still fails fast on real errors (e.g. the 404 -> NotFoundError mapping happens on the awaited call, not the deferred one).<Await> must sit inside a <Suspense> boundary; forgetting that fallback means the whole component throws until the promise settles.Suspense boundary by an id that changes per navigation (key={post.id}) avoids showing stale deferred content when the route re-renders for a new param.wrapInSuspense: true, understanding it here clarifies what's happening under the hood in ch007/ch008.