Capítulo 9 de 24

Chapter 9: Deferred Data

Core Idea

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.

Setup

  • Routing style: code-based (routes built with createRootRoute / createRoute / .addChildren, no routeTree.gen)
  • Key dependencies: @tanstack/react-router, @tanstack/react-router-devtools, redaxios, zod (present in package.json though this file doesn't validate search)
  • Structure: everything lives in one file, src/main.tsx: fetch helpers, root/index/posts/post routes, the deferred-loader logic, and the RouterProvider bootstrap.

Code Example

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>
  )
}
  • What it demonstrates: 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.

Key Takeaways

  1. Only wrap the genuinely slow/optional part of a loader's return value in 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).
  2. <Await> must sit inside a <Suspense> boundary; forgetting that fallback means the whole component throws until the promise settles.
  3. Keying the 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.
  4. This is the client-only shape of the same technique SSR examples wrap with wrapInSuspense: true, understanding it here clarifies what's happening under the hood in ch007/ch008.

Connects To

  • ch008-basic-ssr-streaming-file-based: Uses the same defer/Await idea but on the server, streaming the resolved comments to the client as part of the SSR response instead of resolving purely in the browser.