Data Loading

Load data in TanStack Start routes with server-side query warming and client-side suspense.

VitNode loads data through two primary mechanisms:

  1. Plugin Routes: Loaded through definePluginRoute({ load }) with isomorphic data passing.
  2. Host App Routes: Warmed in the route loader via TanStack Query and read in components via useSuspenseQuery.

Quick start

Plugins load data using definePluginRoute. The loader runs during SSR and client navigation, handing typed loaderData to the page component:

plugins/blog/src/routes/announcements-page.tsx
import type { PluginRoutePageProps } from "@vitnode/core/routing"
import { definePluginRoute } from "@vitnode/core/routing"

interface Announcement {
  id: string
  title: string
}

export const route = definePluginRoute({
  load: async ({ context }) => {
    return await fetchAnnouncements(context.locale)
  },
})

const AnnouncementsPage = ({ loaderData }: PluginRoutePageProps<Announcement[]>) => (
  <div className="container mx-auto p-4 flex flex-col gap-4">
    {loaderData.map((item) => (
      <h2 key={item.id} className="text-xl font-semibold">
        {item.title}
      </h2>
    ))}
  </div>
)

export default AnnouncementsPage

2. In an Application Route File

For application-owned routes, warm TanStack Query in the route loader and consume it with useSuspenseQuery:

Step 1: Define the Query

apps/web/src/features/announcements/query.ts
import { queryOptions } from "@tanstack/react-query"
import { RECORD_STALE_TIME } from "@vitnode/core/lib/query-freshness"

export const announcementsQueryKey = () => ["announcements"] as const

export const announcementsQuery = () =>
  queryOptions({
    queryKey: announcementsQueryKey(),
    queryFn: async () => await fetchAnnouncements(),
    staleTime: RECORD_STALE_TIME,
  })

Step 2: Warm in Route Loader

apps/web/src/routes/_main/announcements.tsx
import { createFileRoute } from "@tanstack/react-router"
import { announcementsQuery } from "#/features/announcements/query"

export const Route = createFileRoute("/_main/announcements")({
  loader: async ({ context }) =>
    await context.queryClient.ensureQueryData({
      ...announcementsQuery(),
      revalidateIfStale: true,
    }),
  component: AnnouncementsPage,
})

Step 3: Consume in Component

apps/web/src/features/announcements/announcements-page.tsx
import { useSuspenseQuery } from "@tanstack/react-query"
import { announcementsQuery } from "./query"

export const AnnouncementsPage = () => {
  const { data } = useSuspenseQuery(announcementsQuery())

  return (
    <div className="p-4">
      {data.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  )
}

Invalidating After Mutations

After creating, editing, or deleting a record, invalidate the query key so TanStack Query refetches fresh data:

import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { announcementsQueryKey } from "./query"

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: createAnnouncement,
  onSuccess: async () => {
    await queryClient.invalidateQueries({ queryKey: announcementsQueryKey() })
    toast.success("Announcement published!")
  },
})

Freshness Constants

VitNode provides standard stale times in @vitnode/core/lib/query-freshness:

ConstantDurationUse Case
RECORD_STALE_TIME30 secondsFast-changing user feeds, announcements
STATIC_STALE_TIME5 minutesSite configuration, navigation, permissions

Learn More