Docs/Core Concepts/Data Loading
GitHub

Data Loading

Data loaders run on the server and provide data to your pages. Create a page.data.ts file next to page.ts.

# Basic loader

// src/app/page.data.ts
import type { PageDataLoad } from "@deijose/nix-js-kit";

export const load: PageDataLoad = async () => {
  return { title: "Hello World", posts: ["a", "b", "c"] };
};
// src/app/page.ts
import { html } from "@deijose/nix-js";
import type { PageProps } from "@deijose/nix-js-kit";
import { load } from "./page.data.ts";

export default function HomePage({ data }: PageProps<typeof load>) {
  return html`
    <h1>${data.title}</h1>
    <ul>${data.posts.map((p) => html`<li>${p}</li>`)}</ul>
  `;
}

:::tip PageProps<typeof load> automatically infers the data type from your loader. No manual interface needed. :::

# Load context

Loaders receive a context with params, searchParams, and request:

export const load: PageDataLoad = async ({ params, searchParams, request }) => {
  const slug = params.slug;                    // dynamic route param
  const page = searchParams.get("page") ?? "1"; // query string
  const cookie = request?.headers.get("Cookie"); // SSR cookies

  return { slug, page, user: await getUser(cookie) };
};

# LoadContext

interface LoadContext {
  params: RouteParams;           // { slug: "hello" } or { slug: ["a","b"] }
  searchParams: URLSearchParams; // ?page=1&tab=info
  request?: Request;             // available in SSR/streaming modes
}

:::note The request object is available in SSR (nix-js-kit start), streaming, and the Vite dev server. In pure SSG (nix-js-kit build), it's undefined because there's no HTTP request. :::

# Dynamic routes with generateStaticParams

// src/app/blog/[slug]/page.data.ts
import type { PageDataLoad } from "@deijose/nix-js-kit";

export const load: PageDataLoad = async ({ params }) => {
  const post = await getPost(params.slug as string);
  return { post };
};
// src/app/blog/[slug]/page.ts
import type { GenerateStaticParams } from "@deijose/nix-js-kit";

export const generateStaticParams: GenerateStaticParams = async () => {
  const posts = await getAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
};

# ISR with revalidate

Export revalidate from page.data.ts to enable incremental static regeneration:

// src/app/blog/[slug]/page.data.ts
export const load = async ({ params }) => {
  return { post: await getPost(params.slug) };
};

export const revalidate = 300; // revalidate every 5 minutes

The first request serves the cached HTML. After revalidate seconds, the next request regenerates the page and updates the cache.

# Layout data loaders

layout.data.ts provides data to layouts:

// src/app/layout.data.ts
export const load = async () => {
  return { nav: await getNav() };
};

The layout receives it via LayoutProps.data:

export default function Layout({ children, data }: LayoutProps<typeof load>) {
  return html`<nav>${data.nav.map(...)}</nav><main>${children}</main>`;
}

# Request context for auth

// src/app/dashboard/page.data.ts
export const load = async ({ request }) => {
  if (!request) return { user: null };

  const session = parseSession(request.headers.get("Cookie"));
  if (!session) {
    throw new Response("Unauthorized", { status: 401 });
  }

  return { user: await getUser(session.userId) };
};

:::warning Cookies are forwarded to loaders in start, preview, dev, and the Vite plugin. Make sure your CSRF and auth middleware run before loaders if you need to block requests early. :::

✎ Edit this page on GitHub