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.
Defining a Prefetch Handler
Section titled “Defining a Prefetch Handler”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:
| Property | Type | Description |
|---|---|---|
params | Record<string, string> | The route parameters extracted from the destination URL. |
url | URL | The full destination URL as a URL object. |
controller | NavigationPrecommitController | The Navigation API’s precommit controller for redirects. |
Integrating with λ Query
Section titled “Integrating with λ Query”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.
Using URL Search Parameters
Section titled “Using URL Search Parameters”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);Prefetch from Links
Section titled “Prefetch from Links”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.
Chaining Prefetches with Groups
Section titled “Chaining Prefetches with Groups”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.
Redirects in Prefetch
Section titled “Redirects in Prefetch”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.
Async Prefetch
Section titled “Async Prefetch”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, anduseQuery()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.