Navigated to /docs/getting-started/fetching-data

Fetching data

Load route-critical data, design pending and error views, stream secondary content, and cancel obsolete work.

Load data the route needs

A page or layout loader runs during route resolution. Its return value becomes page data and is serialized into SSR output, so the browser can hydrate without immediately repeating the request. The page's default component and controller do not mount until its loader completes.

Create src/pages/dashboard.tsx

TSX
tsximport type {
  PageErrorProps,
  PageLoadContext,
  PagePendingProps,
  PageProps,
} from "@tavojs/core/router";
import { Page, Stack, Text } from "@tavojs/ui";

type DashboardData = {
  projects: Array<{ id: string; name: string }>;
  summary: { active: number };
};

export function pending({ pathname }: PagePendingProps) {
  return (
    <Page aria-busy="true" aria-label={`Loading ${pathname}`}>
      <Stack gap="md">
        <Text as="h1" variant="h1">
          Loading dashboard…
        </Text>
        <Text color="muted">Preparing projects and their summary.</Text>
      </Stack>
    </Page>
  );
}

export function error({ pathname }: PageErrorProps) {
  return (
    <Page>
      <Stack gap="md" role="alert">
        <Text as="h1" variant="h1">
          Could not load the dashboard
        </Text>
        <Text color="muted">
          The data for {pathname} is unavailable. Try again.
        </Text>
      </Stack>
    </Page>
  );
}

export async function load({
  signal,
  url,
}: PageLoadContext): Promise<DashboardData> {
  const [projectsResponse, summaryResponse] = await Promise.all([
    fetch(new URL("/api/projects", url), { signal }),
    fetch(new URL("/api/projects/summary", url), { signal }),
  ]);
  if (!projectsResponse.ok || !summaryResponse.ok) {
    throw new Error("Dashboard data could not be loaded");
  }
  return {
    projects: await projectsResponse.json(),
    summary: await summaryResponse.json(),
  } as DashboardData;
}

export default function DashboardPage({ data }: PageProps<DashboardData>) {
  return (
    <Page>
      <Stack gap="md">
        <Text>Active projects: {data?.summary.active ?? 0}</Text>
        {data?.projects.map((project) => (
          <Text key={project.id}>{project.name}</Text>
        ))}
      </Stack>
    </Page>
  );
}

Design the route while its loader is unresolved

The example above exports pending for immediate feedback during active browser resolution and error for a contextual page-loader failure. Tavo.js renders either component inside the target route's resolved layouts.

The browser changes the URL, runs route middleware, resolves layout loaders, renders pending, and then runs the page loader. Success replaces it with the default page; failure replaces it with the page's error component.

Resolution pathPending exportVisible result
Initial CSR resolutionRendered while the page loader runsCompleted page or route error
Later client navigationRendered after layout loaders resolveCompleted page or route error
Normal SSR or static generationNot renderedServer waits for the completed page or error
Prefetch or fresh route-cache hitNot renderedNo visible route replacement during prefetch
  • PagePendingProps contains pathname, params, layers, and layerData; page loader data is intentionally unavailable.

  • PageErrorProps contains those route fields plus data and error.

  • Layout data is available because Tavo.js resolves target layout loaders before showing the page pending component.

  • If the page has no pending export, the previous page remains visible and the route content region is marked busy.

Know when the loader runs

A universal loader follows the route resolver. The table shows why its imports and return value must be safe in every environment that can execute or receive them.

Entry pathWhere load runsWhat the browser receives
Direct CSR visitBrowserNo serialized loader result
Direct SSR visitServerResult is serialized for hydration
HydrationBrowser reuses server dataNo immediate repeat
Later client navigationBrowserFresh result belongs to that navigation

Choose the narrowest data owner

Avoid copying loader results into global stores. Page props already keep request data scoped to the navigation that produced it.

  • Use a page loader when the route cannot render meaningfully without the data.

  • Use a layout loader for request data shared by its descendant routes.

  • Use createResource for component-scoped browser data that can load independently.

  • Use lazy when the asynchronous work is loading a component implementation.

Stream secondary server content

Resolve data required for navigation, SEO, and the primary shell in the loader. Put slower optional server work behind Deferred boundaries with a stable ID, useful fallback, timeout behavior, and an error fallback.

Promise-backed Deferred content progressively patches an SSR stream. For browser-only asynchronous work, use a loader, resource, controller, or store instead.

Pass cancellation through every layer

Navigation owns loader and middleware signals. Replacing a navigation aborts its route work, removes its pending component, and prevents obsolete data or errors from replacing the active route. A resource owns its current load. Deferred work belongs to its supplied signal or render lifecycle. Forward the signal to fetch, database wrappers that support it, and other cancellable clients.

  • Treat AbortError as normal control flow.

  • Do not publish results after their owner has been replaced.

  • Use transactions or idempotency for side effects because cancellation cannot undo a committed mutation.

Checkpoint

Next steps