Routing

Loading States

Render instant skeleton shapes and pending spinners while TanStack Start routes load data or download chunks.

While a route loader executes or a lazy page chunk downloads, VitNode provides instant visual feedback so users never see an unresponsive screen.

Loading states operate at two distinct layers:

  1. Global router transitions — A non-intrusive spinner rendered when navigating between routes.
  2. Component-level Suspense — Purpose-built skeleton shapes matching your layout while asynchronous data streams in.

Global router pending state

TanStack Router displays a pending component during route transitions. VitNode configures this globally in router.tsx using RoutePendingSpinner with built-in debouncing:

apps/web/src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { RoutePendingSpinner } from '@vitnode/core/tanstack/pending'

export function getRouter() {
  return createTanStackRouter({
    routeTree,
    defaultPendingComponent: RoutePendingSpinner,
    defaultPendingMs: 150,
    defaultPendingMinMs: 300,
  })
}
  • defaultPendingMs: 150 — Prevents visual flickering on fast network responses by waiting 150ms before showing the spinner.
  • defaultPendingMinMs: 300 — Ensures that once shown, the spinner remains visible for at least 300ms to avoid jarring flashes.

Component skeletons with Suspense

For feature pages with multiple asynchronous queries, wrap content in React <Suspense> using VitNode's pre-built pending skeletons:

plugins/site-notes/src/pages/notes-page.tsx
import { FeedPendingSkeleton } from '@vitnode/core/tanstack/pending'
import { Suspense } from 'react'

const NotesList = () => {
  // Asynchronously fetched notes
  return <div>Notes List</div>
}

const NotesPage = () => {
  return (
    <div className="container mx-auto max-w-3xl p-4">
      <Suspense fallback={<FeedPendingSkeleton rows={4} />}>
        <NotesList />
      </Suspense>
    </div>
  )
}

export default NotesPage

Pre-built skeleton shapes

Import pre-built skeletons directly from @vitnode/core/tanstack/pending:

Skeleton shapeLayout patternTypical use case
FeedPendingSkeletonCard timeline with avatars and text rowsActivity feeds, note lists, blog posts
TablePendingSkeletonSearch toolbar, column headers, and rowsAdminCP data tables, member directories
CardsPendingSkeletonResponsive 1/2/3 column card gridDashboard widgets, product catalogs
FormPendingSkeletonStacked input placeholders and action buttonsSettings forms, modal dialogs
AuthPendingSkeletonCentered authentication card with form inputsSign-in, sign-up, password reset
RoutePendingSpinnerCentered accessible spinnerFull-page transition fallbacks

Customizing skeleton props

All skeleton shapes accept props from RoutePendingSkeletonProps to adapt to your layout:

plugins/site-notes/src/pages/notes-admin-page.tsx
import { TablePendingSkeleton } from '@vitnode/core/tanstack/pending'
import { Suspense } from 'react'

const AdminNotesPage = () => {
  return (
    <Suspense
      fallback={
        <TablePendingSkeleton
          className="p-6"
          rows={8}
          withDescription={false}
        />
      }
    >
      <AdminNotesTable />
    </Suspense>
  )
}

export default AdminNotesPage
PropTypeDefaultDescription
rowsnumber3 to 6Number of skeleton items, rows, or cards to display
withDescriptionbooleantrueWhether to display a skeleton subtitle under the heading
classNamestringundefinedAdditional Tailwind utility classes merged onto the container
labelstring'Loading'Accessible screen-reader label (aria-label)