Data Fetching
Loaders
Each segment may declare a loader — the equivalent of server-side data fetching in a Next.js segment. Loaders of all matched segments run in parallel on every navigation; results land in context.data of that segment's page and layout:
{
path: "blog/[slug]",
loader: async ({ params, searchParams }) => {
const response = await fetch(`/api/posts/${params.slug}`)
if (!response.ok) notFound()
return response.json()
},
page: (context: RouteContext<Post>) => ({ h1: context.data.title }),
}
The LoaderContext carries pathname, url, params, searchParams and (on the server) headers.
A page can also read ancestor data through context.segmentData, keyed by segment id (e.g. "/", "/blog/[slug]").
Caching with revalidate
revalidate is the ISR-style cache lifetime in seconds:
{
path: "products",
loader: fetchProducts,
revalidate: 60, // cached for 60s, then re-fetched
page: ProductsPage,
}
undefined(default) — re-run on every navigation, likecache: "no-store"revalidate: 60— serve the cached result for 60 secondsrevalidate: Infinity— cache forever, likeforce-cache
router.refresh() clears the cache and re-runs everything for the current URL.
Stale-while-revalidate
When a cached entry ages past its revalidate window, the next navigation serves the stale value immediately and refetches in the background — the same model as Next.js ISR. There is no loading flash: the page renders instantly with the old data, then re-renders in place once the fresh value lands.
{
path: "dashboard",
loader: fetchStats,
revalidate: 30, // after 30s: show stale, refetch, swap in fresh
page: StatsPage,
}
The background refetch starts after the current render commits, so a stale entry never blocks navigation. If the user has already moved to another route by the time it resolves, the re-render is skipped. A failed background refetch keeps the stale entry and stays silent.
Prefetching
router.prefetch(href) (and navLink's hover/visible prefetch) runs the target's loaders ahead of navigation. Results stay usable for 30 seconds — the same window as the Next.js client router cache — so the navigation itself renders instantly without re-fetching, even for uncached loaders.
Control Flow from Loaders
Loaders can interrupt rendering with the next/navigation functions:
import { notFound, redirect, permanentRedirect } from "@domphy/app"
loader: async ({ params }) => {
const session = await getSession()
if (!session) redirect("/login")
const post = await fetchPost(params.slug as string)
if (!post) notFound()
return post
}
redirect() restarts navigation at the target (history entry replaced); notFound() renders the nearest notFound boundary; errors render the nearest error boundary.
Pairing with @domphy/query
Loaders cover route-level data. For cache-rich client data (background refetching, mutations, infinite queries), use @domphy/query inside pages — the two compose freely: the loader provides the initial data, an observer keeps it fresh.