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
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 path | Pending export | Visible result |
|---|---|---|
| Initial CSR resolution | Rendered while the page loader runs | Completed page or route error |
| Later client navigation | Rendered after layout loaders resolve | Completed page or route error |
| Normal SSR or static generation | Not rendered | Server waits for the completed page or error |
| Prefetch or fresh route-cache hit | Not rendered | No visible route replacement during prefetch |
PagePendingPropscontainspathname,params,layers, andlayerData; page loader data is intentionally unavailable.PageErrorPropscontains those route fields plusdataanderror.Layout data is available because
Tavo.jsresolves target layout loaders before showing the page pending component.If the page has no
pendingexport, 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 path | Where load runs | What the browser receives |
|---|---|---|
| Direct CSR visit | Browser | No serialized loader result |
| Direct SSR visit | Server | Result is serialized for hydration |
| Hydration | Browser reuses server data | No immediate repeat |
| Later client navigation | Browser | Fresh 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
createResourcefor 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
AbortErroras 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.