Skip to content

Prefetching

Prefetching in λ Router lets you load data before a navigation completes. Prefetch handlers run during the precommit phase of the Navigation API — after the navigate event fires but before the browser’s URL bar updates. This is the ideal moment to warm your data cache (e.g., with λ Query) so the destination component renders instantly without suspending.

Attach a prefetch handler to a route using the .prefetch() method on the route builder:

import { createRouter } from "@studiolambda/router/react";
const router = createRouter(function (route) {
route("/user/:id")
.prefetch(async function ({ params, url, controller }) {
await fetch(`/api/users/${params.id}`);
})
.render(UserProfile);
});

The prefetch function receives a PrefetchContext object with three properties:

PropertyTypeDescription
paramsRecord<string, string>The route parameters extracted from the destination URL.
urlURLThe full destination URL as a URL object.
controllerNavigationPrecommitControllerThe Navigation API’s precommit controller for redirects.

The primary use case for prefetching is warming the λ Query cache so data is available synchronously when the component renders. Since both the router and query instances live inside React components, the query instance is accessible via closure:

import { createRouter, Router } from "@studiolambda/router/react";
import { createQuery, QueryProvider, useQuery } from "@studiolambda/query/react";
function App() {
const query = createQuery();
const router = createRouter(function (route) {
route("/user/:id")
.prefetch(function ({ params }) {
// Warm the cache — useQuery("/api/users/42") will resolve
// synchronously when UserProfile renders.
query.query(`/api/users/${params.id}`);
})
.render(UserProfile);
route("/posts")
.prefetch(function () {
query.query("/api/posts");
})
.render(PostsList);
});
return (
<QueryProvider query={query}>
<Router matcher={router} />
</QueryProvider>
);
}
function UserProfile() {
const { id } = useParams();
const { data } = useQuery(`/api/users/${id}`);
// `data` is available immediately — no Suspense fallback shown.
return <h1>{data.name}</h1>;
}

Because query.query() populates the items cache and λ Query deduplicates in-flight requests, the useQuery() call in the component returns the already-cached (or already-in-flight) promise. If the data resolved during precommit, use() in React reads it synchronously — no suspension.

The url property gives you access to the full destination URL, including search parameters:

route("/search")
.prefetch(function ({ url }) {
const term = url.searchParams.get("q");
if (term) {
query.query(`/api/search?q=${encodeURIComponent(term)}`);
}
})
.render(SearchResults);

The Link component can trigger prefetch handlers proactively before any navigation occurs, using either hover or viewport detection:

import { Link } from "@studiolambda/router/react";
// Prefetch when the user hovers over the link.
<Link href="/dashboard" prefetch="hover">Dashboard</Link>
// Prefetch when the link scrolls into the viewport.
<Link href="/heavy-page" prefetch="viewport">Heavy Page</Link>

When prefetching from a link, the PrefetchContext is fully populated with the matched params and parsed url, but the controller is a stub (no-op redirect and addHandler) since there is no real navigation event. Your prefetch handlers should work identically regardless — just focus on data loading, not controller manipulation.

The once prop (default true) ensures the prefetch only runs once per link instance, preventing redundant cache warming on repeated hovers.

When using route groups, parent prefetch handlers run before child prefetch handlers, sequentially:

const router = createRouter(function (route) {
const dashboard = route("/dashboard")
.prefetch(function () {
// Runs first — load shared dashboard data.
query.query("/api/dashboard/layout");
})
.group();
dashboard("/analytics")
.prefetch(function () {
// Runs second — load analytics-specific data.
query.query("/api/analytics/overview");
})
.render(Analytics);
dashboard("/settings")
.prefetch(function () {
// Runs second — load settings-specific data.
query.query("/api/settings");
})
.render(Settings);
});

When navigating to /dashboard/analytics, the shared dashboard prefetch runs first, then the analytics prefetch. Both run before the URL commits.

The controller property on PrefetchContext allows redirecting during the precommit phase. This is what .redirect() uses internally:

route("/protected")
.prefetch(function ({ controller }) {
if (!isAuthenticated()) {
controller.redirect("/login");
}
})
.render(ProtectedPage);

The redirect happens before the URL bar updates, so the user never sees the protected URL. Prefer using .redirect() for simple cases and middleware for auth guards — use controller.redirect() in prefetch only when the redirect decision depends on data you’re loading.

Prefetch handlers can be async. The Navigation API will wait for the promise to resolve before committing the URL:

route("/post/:slug")
.prefetch(async function ({ params }) {
// The URL won't commit until this resolves.
await query.query(`/api/posts/${params.slug}`);
})
.render(BlogPost);

Whether you await the query.query() call depends on your desired behavior:

  • With await: The URL commits only after data is loaded. The user sees the old page longer, but the new page appears fully loaded.
  • Without await: The URL commits immediately. The data request is in-flight, and useQuery() in the component deduplicates it. The component may briefly suspend if data hasn’t arrived yet.

Both approaches are valid — choose based on your UX preferences.