T
TanStack2mo ago
graceful-blue

Couldn't get pendingComponent to render

I know I'm using createServerFn from Start, but I think this is more of a Router question, so I think it fits here. Anyway. I'm working on a fresh Start project via pnpm create @tanstack/start@latest. I'm still new to the whole Tanstack stack. I'm reading the doc about Data Loading, and in the Showing a pending component section, it is said that pendingComponent will be shown when it takes more than 1 sec for the loader to resolve. EDIT It actually works on page navigation, but not on browser page refresh, which was what I tested originally. I'm still wrapping my head around SSR, but I assume this is working as intended.
Data Loading | TanStack Router React Docs
Data loading is a common concern for web applications and is related to routing. When loading a page for your app, it's ideal if all of the page's async requirements are fetched and fulfilled as early...
4 Replies
graceful-blue
graceful-blueOP2mo ago
Let say I have this fetcher function
import { createServerFn } from '@tanstack/react-start';

export const fetchPost = createServerFn({ method: 'GET' }).handler(async () => {
// Simulate a slow request
await new Promise((resolve) => setTimeout(resolve, 3000));
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/1`);
const post = await res.json();
return { post };
});
import { createServerFn } from '@tanstack/react-start';

export const fetchPost = createServerFn({ method: 'GET' }).handler(async () => {
// Simulate a slow request
await new Promise((resolve) => setTimeout(resolve, 3000));
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/1`);
const post = await res.json();
return { post };
});
So my first impression is this would trigger the pending component (this is a common data fetching pattern shown throughout the doc and Youtube tutorials)
// 1: Common data access pattern
import { createFileRoute } from '@tanstack/react-router';

import { fetchPost } from '@/server/posts';

export const Route = createFileRoute('/posts/$id')({
loader: async () => {
const data = await fetchPost();
return data;
},
pendingComponent: () => <div>Loading...</div>,
notFoundComponent: () => <div>Not found</div>,
errorComponent: () => <div>Error</div>,
component: RouteComponent,
});

function RouteComponent() {
const { post } = Route.useLoaderData() as any;
return (
<div>
<div className="text-lg font-bold capitalize">{post.title}</div>
<div className="text-gray-500">{post.body}</div>
</div>
);
}
// 1: Common data access pattern
import { createFileRoute } from '@tanstack/react-router';

import { fetchPost } from '@/server/posts';

export const Route = createFileRoute('/posts/$id')({
loader: async () => {
const data = await fetchPost();
return data;
},
pendingComponent: () => <div>Loading...</div>,
notFoundComponent: () => <div>Not found</div>,
errorComponent: () => <div>Error</div>,
component: RouteComponent,
});

function RouteComponent() {
const { post } = Route.useLoaderData() as any;
return (
<div>
<div className="text-lg font-bold capitalize">{post.title}</div>
<div className="text-gray-500">{post.body}</div>
</div>
);
}
but it didn't. It seems that the rendering is blocked until the promises are resolved. I've tried using React 19 use hook and wrap the consumer component in Suspense
// 2: Using React 19 use hook and wrap in Suspense / Await

import { createFileRoute } from '@tanstack/react-router';
import { Await, Suspense, use } from 'react';

import { fetchPost } from '@/server/posts';

export const Route = createFileRoute('/posts/$id')({
loader: async () => {
const postPromise = fetchPost();
return { postPromise };
},
pendingComponent: () => <div>Loading...</div>,
notFoundComponent: () => <div>Not found</div>,
errorComponent: () => <div>Error</div>,
component: RouteComponent,
});

function Post() {
const { postPromise } = Route.useLoaderData();
const { post } = use(postPromise);

return (
<div>
<div className="text-lg font-bold capitalize">{post.title}</div>
<div className="text-gray-500">{post.body}</div>
</div>
);
}

function RouteComponent() {
return (
<Suspense fallback={<div>Fallback Loading...</div>}>
<Post />
</Suspense>
);
}

// Tried this too
// <Await promise={postPromise} fallback={<div>Loading...</div>}>
// {({ post }) => {
// return (
// <div>
// <div className="text-lg font-bold capitalize">{post.title}</div>
// <div className="text-gray-500">{post.body}</div>
// </div>
// );
// }}
// </Await>
// 2: Using React 19 use hook and wrap in Suspense / Await

import { createFileRoute } from '@tanstack/react-router';
import { Await, Suspense, use } from 'react';

import { fetchPost } from '@/server/posts';

export const Route = createFileRoute('/posts/$id')({
loader: async () => {
const postPromise = fetchPost();
return { postPromise };
},
pendingComponent: () => <div>Loading...</div>,
notFoundComponent: () => <div>Not found</div>,
errorComponent: () => <div>Error</div>,
component: RouteComponent,
});

function Post() {
const { postPromise } = Route.useLoaderData();
const { post } = use(postPromise);

return (
<div>
<div className="text-lg font-bold capitalize">{post.title}</div>
<div className="text-gray-500">{post.body}</div>
</div>
);
}

function RouteComponent() {
return (
<Suspense fallback={<div>Fallback Loading...</div>}>
<Post />
</Suspense>
);
}

// Tried this too
// <Await promise={postPromise} fallback={<div>Loading...</div>}>
// {({ post }) => {
// return (
// <div>
// <div className="text-lg font-bold capitalize">{post.title}</div>
// <div className="text-gray-500">{post.body}</div>
// </div>
// );
// }}
// </Await>
However, it's still not displaying the pending component. Finally, I've tried using Query
import { createFileRoute } from '@tanstack/react-router';
import { Suspense } from 'react';

import { fetchPost } from '@/server/posts';
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';

const postQueryOptions = queryOptions({
queryKey: ['post'],
queryFn: () => fetchPost(),
});

export const Route = createFileRoute('/posts/$id')({
loader: async ({ context }) => {
context.queryClient.prefetchQuery(postQueryOptions);
},
pendingComponent: () => <div>Loading...</div>,
notFoundComponent: () => <div>Not found</div>,
errorComponent: () => <div>Error</div>,
component: RouteComponent,
});

function Post() {
const {
data: { post },
} = useSuspenseQuery(postQueryOptions);

return (
<div>
<div className="text-lg font-bold capitalize">{post.title}</div>
<div className="text-gray-500">{post.body}</div>
</div>
);
}

function RouteComponent() {
return (
<Suspense fallback={<div>Fallback Loading...</div>}>
<Post />
</Suspense>
);
}
import { createFileRoute } from '@tanstack/react-router';
import { Suspense } from 'react';

import { fetchPost } from '@/server/posts';
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';

const postQueryOptions = queryOptions({
queryKey: ['post'],
queryFn: () => fetchPost(),
});

export const Route = createFileRoute('/posts/$id')({
loader: async ({ context }) => {
context.queryClient.prefetchQuery(postQueryOptions);
},
pendingComponent: () => <div>Loading...</div>,
notFoundComponent: () => <div>Not found</div>,
errorComponent: () => <div>Error</div>,
component: RouteComponent,
});

function Post() {
const {
data: { post },
} = useSuspenseQuery(postQueryOptions);

return (
<div>
<div className="text-lg font-bold capitalize">{post.title}</div>
<div className="text-gray-500">{post.body}</div>
</div>
);
}

function RouteComponent() {
return (
<Suspense fallback={<div>Fallback Loading...</div>}>
<Post />
</Suspense>
);
}
Again. Still not showing. So now I'm running out of ideas. Maybe I'm missing something? My deps FYI
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-router": "^1.133.22",
"@tanstack/react-router-devtools": "^1.133.22",
"@tanstack/react-router-ssr-query": "^1.133.22",
"@tanstack/react-start": "^1.133.22",
"@tanstack/router-plugin": "^1.133.22",
"lucide-react": "^0.544.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.0.6",
"vite-tsconfig-paths": "^5.1.4"
}
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-router": "^1.133.22",
"@tanstack/react-router-devtools": "^1.133.22",
"@tanstack/react-router-ssr-query": "^1.133.22",
"@tanstack/react-start": "^1.133.22",
"@tanstack/router-plugin": "^1.133.22",
"lucide-react": "^0.544.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.0.6",
"vite-tsconfig-paths": "^5.1.4"
}
Okay, I just checked again. The pendingComponent works when the page is loaded via in-app navigation (e.g. the Link component). It didn't work when the page first loaded and reloaded. I assume this is the expected behavior
graceful-blue
graceful-blueOP2mo ago
GitHub
GitHub - akmalmzamri/tanstack-start-data-loading-patterns: Common d...
Common data loading and access patterns in Tanstack Start - akmalmzamri/tanstack-start-data-loading-patterns
sensitive-blue
sensitive-blue2mo ago
this is start related, hence not the right place, so next time please use #start-questions in start, the initial response will block until the loaders are finished if you dont want that, return a nested promise (e.g. in an object) from a loader then uses that promise or <Await> it in your component to render the pending component
graceful-blue
graceful-blueOP2mo ago
Thanks

Did you find this page helpful?