Guides
Link and Navigation
Use Link for app navigation that should participate in Mreact's client navigation runtime. It gives you route-aware prefetching, scroll behavior, and controlled document reloads without turning a server-rendered route into a hydrated client route.
Use Link for app navigation
Use Link from @reckona/mreact-router/link for internal app links. Raw anchors are still valid for plain HTML and external destinations, but Link is the right default when you want route navigation behavior.
import { Link } from "@reckona/mreact-router/link";
export default function Page() {
return <Link href="/docs">Docs</Link>;
}Typed route hrefs
Concrete Link hrefs can be typed directly. Build output includes .mreact/routes.d.ts, which declares discovered app route patterns and augments @reckona/mreact-router/link. Include that declaration in your TypeScript program, and Link will type-check internal href strings without importing any generated runtime module.
The generated file contains declare module "@reckona/mreact-router/link" so the public Link type learns your app's route union at type-check time.
{
"include": ["src", ".mreact/routes.d.ts"]
}The same generated declaration exports AppRouteParams<Path> and AppRouteParamMap for route-aware helper code that needs the params shape without duplicating folder names by hand. A route such as src/app/users/$id/files/$...path/page.tsx maps to AppRouteParams<"/users/:id/files/:...path"> with { readonly id: string; readonly path: readonly string[] }, so renaming a route folder updates the generated params type on the next build.
For dynamic routes, pass a concrete URL to Link. The generated declaration checks that the URL matches a discovered route shape, so dynamic params are required at compile time as path segments, unknown route shapes are rejected, and external URLs stay outside the internal route type.
import { Link } from "@reckona/mreact-router/link";
export function UserLink(props: { user: { id: string; name: string } }) {
return (
<Link href={`/users/${encodeURIComponent(props.user.id)}?tab=activity#profile`}>
{props.user.name}
</Link>
);
}For catch-all routes, encode each decoded segment and join them into the concrete URL:
import { Link } from "@reckona/mreact-router/link";
export function FileLink(props: { path: readonly string[] }) {
const encodedPath = props.path.map(encodeURIComponent).join("/");
return (
<Link href={`/files/${encodedPath}`}>
Open file
</Link>
);
}Use href() only for runtime construction
Use href() from @reckona/mreact-router when code needs runtime URL construction from Mreact route patterns, especially when you want one helper to encode params, search values, and hashes. It remains public and safe to use, but it is no longer required just to get typed Link hrefs.
import { href } from "@reckona/mreact-router";
import { Link } from "@reckona/mreact-router/link";
export function UserLink(props: { user: { id: string; name: string } }) {
return (
<Link
href={href("/users/:id", {
hash: "profile",
params: { id: props.user.id },
search: { tab: "activity" },
})}
>
{props.user.name}
</Link>
);
}Importing href() into a client route includes the URL-building helper in that route's client bundle. For ordinary links with already-constructed strings, prefer Link href directly.
Validated URL search state
defineSearchState() gives each search field a typed codec and default. parse() can validate a server request URL, while get() reads the current browser URL. Invalid or duplicate values fall back to the field default and appear in invalid. Unknown keys are also reported. format() omits defaults.
import { defineSearchState, searchParam } from "@reckona/mreact-router";
const projects = defineSearchState({
status: searchParam.oneOf(["open", "closed"] as const, "open"),
page: searchParam.integer(1, { min: 1 }),
});
const { value, invalid } = projects.parse(new URL(request.url).search);
// value.page is a number; invalid contains malformed keys.In browser code, projects.set({ page: 3 }, { history: "replace" }) navigates through the app router so loaders and route HTML still update. It resolves to false when client navigation is unavailable. projects.subscribe("page", callback) runs only after a committed navigation changes the parsed page value; dispose the returned listener with the component. The state helper is opt-in client code and contributes to that route's JavaScript budget.
Restore view state on back and forward
Use createNavigationSnapshotStore() at module scope to save JSON values for the current history entry. A list page can save its selected row, expanded keys, virtual list offset, or draft fields before opening a detail page, then load them when the user returns. This store keeps values in memory within the current document; a full reload starts fresh. It does not retain DOM nodes or keep hidden components running.
import { createNavigationSnapshotStore } from "@reckona/mreact-router";
const snapshots = createNavigationSnapshotStore({ maxEntries: 20, ttlMs: 30 * 60_000 });
snapshots.save("projects", { selectedId: "p3", scrollOffset: 420 });
const restored = snapshots.load<{ selectedId: string; scrollOffset: number }>("projects");Values are limited to 32 KiB each by default, with at most 32 keys and 128 KiB per history entry. save() returns false for unsupported or oversized values. Call snapshots.clear() on logout or tenant changes. Validate restored values before using them as trusted application data. The store follows router-managed history; direct history.pushState() and history.replaceState() calls are outside this contract.
Prefetch, not preload
The API is named prefetch, not preload. It controls navigation preparation, not server runtime module preloading or HTML asset preload links.
import { Link } from "@reckona/mreact-router/link";
export function Navigation() {
return (
<nav>
<Link href="/dashboard" prefetch="intent">Dashboard</Link>
<Link href="/docs" prefetch="viewport">Docs</Link>
<Link href="/settings" prefetch="none">Settings</Link>
</nav>
);
}prefetch="intent" is the default behavior for likely user intent. prefetch="viewport" prepares links when they enter the viewport. prefetch="none" disables route prefetch for links where eager work would be wasteful or surprising.
For client routes, intent prefetch warms both the route's navigation HTML and its entry script dependencies, so a click can reuse the in-flight or completed HTML request. Viewport prefetch keeps client-route work to script and modulepreload hints; it does not fetch HTML until intent is detected. Server-only routes retain HTML warming for viewport links. Cacheable prefetched navigation HTML is kept in a bounded browser cache for up to 30 seconds and is discarded after mutations or explicit revalidation. Responses with Cache-Control: no-store or no-cache are never saved in that cache, but a navigation can share an already pending prefetch while its request context remains unchanged. Once that prefetch finishes, a later navigation fetches fresh HTML.
Scroll and document reloads
Use scroll="preserve" when the current scroll position should survive navigation, such as editor panes or long virtualized views. Use reload when a link should force a full document navigation instead of app navigation.
import { Link } from "@reckona/mreact-router/link";
export function ToolbarLinks() {
return (
<nav>
<Link href="/editor" scroll="preserve">Editor</Link>
<Link href="/legacy" reload>Legacy page</Link>
</nav>
);
}Navigation runtime
Server-only routes can get the lightweight navigation runtime without becoming hydrated client routes. When Mreact can see a rendered Link in the server-rendered route tree, it injects the navigation runtime automatically.
Use navigationRuntime only to override automatic detection:
export const navigationRuntime = false;Set it to false when a route must stay JavaScript-free even though it renders Link. Set it to true when navigation links are hidden behind a pattern the analyzer cannot see, such as a render prop, higher-order component, or runtime props value.
Same-origin navigation
Mreact intercepts same-origin app navigation and updates the changed route payload without a full document reload. During client navigation, it keeps route data, metadata, scroll restoration, and client route scripts synchronized.
When navigations overlap, the latest navigation owns the DOM update, history change, and pending state. A slower response from an earlier navigation cannot restore its destination or trigger a fallback document reload after it has been superseded.
External URLs use normal document navigation. Use reload for an internal URL when the browser should also perform a normal document navigation.