Loaders
Loaders in the Routing and data guide.
A loader is an ordinary function. The router calls it on the server for the first request and in the browser for every
client navigation afterward, so a loader is isomorphic: it must not touch a database, the file system or a secret.
Call a server function (the browser build turns it into a request) or fetch an
API route. What the server loaded is embedded in the page (<script id="__shadow_router">) and used to hydrate, so
the browser does not run the loader again for the first render.
import { A, useLoaderData } from "@shadow-js/router";
import type { HeadTag, JSX } from "@shadow-js/web";
import { listPosts, type Post } from "../../server/posts";
export const loader = (): Promise<Post[]> => listPosts();
export const head = (): HeadTag[] => [{ tag: "title", children: "Posts" }];
export default function PostList(): JSX.Element {
const posts = useLoaderData<Post[]>();
return (
<ul>
{(posts() ?? []).map((post) => (
<li>
<A to="/posts/:id" params={{ id: post.id }}>
{post.title}
</A>
</li>
))}
</ul>
);
}
import { type LoaderArgs, useLoaderData, useSearch } from "@shadow-js/router";
import { HttpError } from "@shadow-js/dusk";
import type { HeadTag, JSX } from "@shadow-js/web";
import { getPost, type Post } from "../../server/posts";
interface Search {
tab: "body" | "comments";
}
export const validateSearch = (search: Record<string, unknown>): Search => {
const tab = search["tab"] ?? "body";
if (tab !== "body" && tab !== "comments") throw new HttpError(400, "Unknown tab");
return { tab };
};
export const loader = ({ params }: LoaderArgs<"/posts/:id", Search>): Promise<Post> =>
getPost({ id: params.id });
export const head = ({ loaderData }: { loaderData: Post | undefined }): HeadTag[] => [
{ tag: "title", children: loaderData?.title ?? "Post" },
];
export const error = (props: { error: unknown }): JSX.Element => (
<p class="post-error">
{props.error instanceof Error ? props.error.message : "Something went wrong."}
</p>
);
export default function PostPage(): JSX.Element {
const post = useLoaderData<Post>();
const search = useSearch<Search>();
return (
<article>
<h3>{post()?.title}</h3>
<p>Showing the {search.tab}.</p>
</article>
);
}
LoaderArgs<"/posts/:id", Search> types params.id as string and search as the validator's result. A route file
cannot infer its own pattern, so you write it; the generated tree checks that the loader, head and validateSearch
agree with the pattern the file system gives the route.
context.event is the request event during server rendering and absent in the browser. Use it only for things that
may differ, never as the only source of truth; a server function reads the request with getRequestEvent() and works
in both places.