Data Loading
Co-locate a server loader with a page; its data is rendered on the server and hydrated on the client — no waterfalls, no client fetch on first paint.
A loader runs on the server before a page renders. What awaits it, renders the page with the resolved data in scope, and serializes that data into the HTML so the client hydrates without re-fetching. This is the data path for SSR and ISR pages.
The loader export
Export a function named loader from a page module. It receives { params, query, request } and returns data (sync or async).
// src/pages/blog/[slug].jsx
export const loader = async ({ params, query, request }) => {
const post = await db.posts.find(params.slug);
return { post };
};
export default function Post() {
const { post } = useLoaderData();
return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}
params— dynamic route segments (e.g.{ slug: 'hello' }for/blog/[slug]).query— parsed query string as an object.request— the standardRequest(read cookies/headers for auth).
Runs before render
The loader resolves outside the synchronous render, so the value handed to the component is plain data — never a promise. That keeps render fast and concurrency-safe.
useLoaderData
useLoaderData() returns the current page's loader data. It is isomorphic: on the server it reads the render-scoped context; on the client it reads the hydration payload (<script id="__what_data">). It is not a hook-slot consumer, so you can call it anywhere in a component.
import { useLoaderData } from 'what-framework';
function Post() {
const { post } = useLoaderData();
// ...
}
getStaticPaths
For dynamic routes that should be pre-rendered, export getStaticPaths. It returns the set of paths to build ahead of time plus a fallback policy for everything else.
export async function getStaticPaths() {
const posts = await db.posts.all();
return {
paths: posts.map((p) => ({ params: { slug: p.slug } })),
fallback: 'blocking', // 'blocking' | true | false
};
}
'blocking'— an unbuilt path renders on first request, then is cached (default-feeling, great for ISR).true— serve a skeleton immediately, then swap in the rendered page.false— anything not inpathsis a 404.
Because getStaticPaths is a function (it can't be JSON), it is a named export, never part of the page config object.
Loaders vs client fetching
Use a loader for data the page needs to render on the server (SEO content, the primary record). Use the client data-fetching hooks (useSWR, useQuery) for data that loads after hydration (user-specific widgets, polling dashboards). They compose: a loader seeds the first paint, hooks keep it live.