Data loading and middleware
Load route data, provide route-specific pending and error states, redirect or gate navigation, and cancel obsolete work.
Load route-critical data
Page and layout loaders run during route resolution. Their result becomes page data and is available to route-aware controllers. A page can export pending for active browser resolution and error for its loader failure. Pass the provided AbortSignal to downstream work so superseded navigation cannot publish stale results.
tsximport type {
PageErrorProps,
PageLoadContext,
PagePendingProps,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { name: string };
export async function load({ params, signal, url }: PageLoadContext): Promise<Project> {
const endpoint = new URL(`/api/projects/${params.id}`, url);
const response = await fetch(endpoint, { signal });
if (!response.ok) throw new Error("Project could not be loaded");
return response.json();
}
export function pending({ params }: PagePendingProps) {
return <Page aria-busy="true"><Text>Loading project {params.id}…</Text></Page>;
}
export function error({ pathname }: PageErrorProps) {
return <Page><Text role="alert">Could not load {pathname}.</Text></Page>;
}
export default function ProjectPage({ data }: PageProps<Project>) {
return <Page><Text>{data?.name}</Text></Page>;
}Keep middleware request-scoped
Middleware runs before loaders and can continue or redirect route resolution. Return nothing to continue, or return an object with redirect and an optional status. A status without redirect does not stop the route. This example normalizes a redundant query before the loader runs. Use defineServerMiddleware plus a server-only helper for sessions, secrets, or other policy that must never run during browser navigation.
tsimport { defineServerMiddleware } from "@tavojs/core/router";
export const middleware = defineServerMiddleware(({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get("view") === "all") {
return { redirect: "/projects", status: 308 };
}
});Route loader or resource?
Starting a new resource load aborts the previous one. Treat cancellation as expected control flow rather than an application error.
Use a loader when the route cannot render meaningfully without the data.
Use a layout loader for data shared by descendant routes.
Use
createResourcefor component-scoped async data that can load independently.Use lazy when the async work is loading a component implementation.
Loader contract
Loader runtime defaults to both.
defineServerLoaderis equivalent to a server-only loader and is skipped during browser resolution.request, URL, Headers, and
AbortSignalare portable Fetch APIs. Build application-relative fetch URLs with new URL(path, url) so the same loader works in Node and the browser. UserawRequestonly at an adapter integration boundary.Pass signal to fetch and every abort-aware dependency. Superseded navigation is expected cancellation and must not publish stale data.
Layout loaders resolve from root to leaf, followed by the page loader. Later loader contexts receive successful earlier results through the optional keyed
context.layersrecord. Rendered page, pending, and error props use a different shape: ordered layers plus keyedlayerData.A layout-loader failure stays on that layout layer and enters route error handling rather than rendering the page pending view with invalid layout data.
A page-loader failure renders the target page's error export when present, then falls back to
src/pages/_error.tsx.An aborted obsolete resolution returns to idle rather than rendering an error.
tstype PageLoadContext = {
pathname: string;
params: Record<string, string>;
request: Request;
rawRequest?: unknown;
url: URL;
headers: Headers;
method: string;
signal: AbortSignal;
layers?: Record<string, unknown>;
};
defineLoader(handler, { runtime?: "server" | "client" | "both" })
defineServerLoader(handler)Middleware contract and order
Middleware can be declared globally, by plugins, on layouts, and on pages. Runtime-wide middleware runs first, then layout middleware from root to leaf, then page middleware.
Return nothing to continue. Return redirect to stop normal resolution; status defaults to 302 and only has an effect when redirect is present. Returning status alone does not block a route.
Middleware runtime defaults to both. Use
defineServerMiddlewareforHttpOnlycookies, secrets, databases, and server sessions.Redirect targets are same-origin by default. External redirects require an explicit runtime opt-in and application validation.
tstype PageMiddleware = ((context: {
to: string;
from?: string;
params: Record<string, string>;
request: Request;
rawRequest?: unknown;
url: URL;
headers: Headers;
method: string;
signal: AbortSignal;
}) => void | { redirect?: string; status?: number } | Promise<...>) & {
__tavo_middleware_options__?: { runtime?: "server" | "client" | "both" };
};Component resource reference
A resource starts idle with null data, error, and
updatedAt. A new load aborts the previous load.preload deduplicates the current pending operation. load always starts a new one.
abort returns to idle, clears error and
updatedAt, and preserves the last data. reset also clears data.An aborted operation resolves to idle state even if the loader ignores its signal. Non-abort failures resolve to error state rather than throwing from load.
tstype ResourceState<T> = {
status: "idle" | "loading" | "success" | "error";
data: T | null;
error: unknown;
updatedAt: number | null;
};
type Resource<T> = {
store: Store<ResourceState<T>>;
read(): ResourceState<T>;
load(options?: { signal?: AbortSignal }): Promise<ResourceState<T>>;
preload(options?: { signal?: AbortSignal }): Promise<ResourceState<T>>;
abort(reason?: unknown): void;
reset(): void;
};Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.