[]previous and next page

Loaders

Loaders in the Routing and data guide.

For agentsUpdated

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.

src/routes/posts/index.tsxtsx
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>
  );
}
src/routes/posts/[id].tsxtsx
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.