Load functions
load lives in +page.server.ts or +layout.server.ts and runs on the server only. Whatever it returns becomes the data prop of the page or layout. It never ships to the browser, so this is where database calls and secrets go.
import type { LoadContext } from "@sigil-dev/grimoire";
export function load({ params }: LoadContext) {
return { book: getBook(Number(params.id)) };
}It can be synchronous or async; use async for anything that awaits.
Context
Every load receives one object:
| Field | Type | What it is |
|---|---|---|
request | Request | The incoming request. |
params | Record<string, string> | Dynamic segments from the URL. |
url | URL | The parsed URL, including the query string. |
locals | Record<string, unknown> | Whatever hooks and plugins put there (hooks). |
fetch | typeof fetch | A fetch that carries cookies and headers across internal requests. |
parent | () => Promise<object> | Data from ancestor layouts, merged outermost-first. |
fetch is the one to reach for instead of the global: a global fetch to your own server drops the session cookie, so a server-side call to a protected route fails where the browser's would succeed.
Reading the query string
export function load({ url }: LoadContext) {
const page = Number(url.searchParams.get("page") ?? "1");
const q = url.searchParams.get("q");
return { page, q, results: search(q) };
}Reading parent layout data
A layout's load runs for every route beneath it. parent() returns the merged data of all ancestors:
export async function load({ parent, locals }: LoadContext) {
const { user } = await parent();
if (!user) throw error(401, "Sign in first.");
return { role: await roleFor(user.id) };
}Merging is outermost-first, so a nearer layout's key wins over a further one's. The page's data is the merge of every layout's return value plus the page's own.
Conditional matching
canMatch runs before load and decides whether this route should handle the request:
export function canMatch({ locals }: LoadContext) {
return Boolean(locals.user);
}Returning false produces a 404. It is not a fallthrough to the next matching route — the route is already the winner by the time canMatch runs, and declining it means "this page does not exist here". Throw redirect() to send the visitor somewhere instead of 404ing, which is usually what you want for an auth gate:
export function canMatch({ locals }: LoadContext) {
if (!locals.user) throw redirect("/login");
return true;
}canMatch works on +layout.server.ts too, and can be async.
Returning promises
Any top-level property can be a promise. The page renders without waiting for it, and the value is patched in when it settles:
export function load({ params }: LoadContext) {
return {
book: getBook(Number(params.id)), // awaited
reviews: getReviews(Number(params.id)), // streamed
};
}The property is undefined for now and reactive once it arrives — reading it in JSX re-renders that part. await it only if the page genuinely cannot render without it; a top-level await on the whole thing throws the benefit away.
The loading export
Export loading from +page.tsx to stream a shell before the data is ready:
export const loading = () => <p>Loading…</p>;The browser paints this the moment the response head arrives, then swaps in the real page. It takes no arguments and no data — it renders before data exists, so it can only use hardcoded or module-level values. Keep it the same shape as the real page so nothing reflows on the swap.
loading is opt-in and only applies to first loads, not client-side navigation. A plugin implementing onRouteRender also disables it, because that hook needs the whole HTML as a string.
Errors
Throw to end the request with a status:
import { error } from "@sigil-dev/grimoire";
export function load({ params }: LoadContext) {
const book = getBook(Number(params.id));
if (!book) throw error(404, "No book with that id.");
return { book };
}error(status, message) renders the nearest +error.tsx. redirect(location) and fail() behave the same way here. All three are thrown, never returned.
Typed context
TypedLoadContext<P> gives you typed params and locals:
import type { TypedLoadContext } from "@sigil-dev/grimoire";
export function load({ params }: TypedLoadContext<{ id: string }>) {
return { book: getBook(Number(params.id)) }; // params.id is string
}Next
- Form actions — handling POST
- Streaming slow data — the long version