@real-router/navigation-plugin
v0.6.0
Published
Navigation API integration plugin for browser URL synchronization
Downloads
1,517
Maintainers
Readme
@real-router/navigation-plugin
Navigation API integration for Real-Router. Drop-in replacement for browser-plugin with route-level history access.
Installation
npm install @real-router/navigation-pluginPeer dependency: @real-router/core
Quick Start
import { createRouter } from "@real-router/core";
import { navigationPluginFactory } from "@real-router/navigation-plugin";
const router = createRouter([
{ name: "home", path: "/" },
{ name: "users", path: "/users/:id" },
]);
router.usePlugin(navigationPluginFactory());
await router.start(); // path inferred from browser locationWhy Navigation API?
The Navigation API (~89% browser support) gives you access to the full session history as structured data. Unlike the History API, you can inspect every entry, check what routes the user has visited, and traverse directly to a specific past entry.
// Not possible with browser-plugin:
router.peekBack(); // what's one step back?
router.hasVisited("checkout"); // did the user visit checkout?
router.getVisitedRoutes(); // all routes in this session
router.traverseToLast("users.list"); // jump back to the last users listOptions
router.usePlugin(
navigationPluginFactory({
base: "/app", // Base path prefix for all routes
forceDeactivate: false, // Respect canDeactivate guards on back/forward (default)
}),
);| Option | Type | Default | Description |
| ----------------- | --------- | ------- | ---------------------------------------------------------------------- |
| base | string | "" | Base path for all routes (e.g., "/app" → URLs start with /app/...) |
| forceDeactivate | boolean | false | If true, browser back/forward skip canDeactivate guards. Default false respects guards — matches browser-plugin. |
Router Extensions
Compatible extensions (same as browser-plugin)
| Method | Returns | Description |
| -------------------------------------------- | -------------------- | ------------------------------------------------ |
| buildUrl(name, params?) | string | Build full URL with base path |
| matchUrl(url) | State \| undefined | Parse URL to router state |
| replaceHistoryState(name, params?) | void | Update browser URL without triggering navigation |
router.buildUrl("users", { id: "123" });
// => "/app/users/123" (with base "/app")
router.matchUrl("/app/users/123");
// => { name: "users", params: { id: "123" }, path: "/users/123" }
// Update URL silently (no transition, no guards)
router.replaceHistoryState("users", { id: "456" });Exclusive extensions (Navigation API only)
| Method | Returns | Description |
| ------------------------------- | ----------------------------- | ----------------------------------------------- |
| peekBack() | State \| undefined | State of the previous history entry |
| peekForward() | State \| undefined | State of the next history entry |
| hasVisited(routeName) | boolean | Whether any history entry matches the route |
| getVisitedRoutes() | string[] | Unique route names across all history entries |
| getRouteVisitCount(routeName) | number | How many history entries match the route |
| traverseToLast(routeName) | Promise<State> | Navigate to the last history entry for a route |
| canGoBack() | boolean | Whether there's a previous history entry |
| canGoForward() | boolean | Whether there's a next history entry |
| canGoBackTo(routeName) | boolean | Whether any previous entry matches the route |
peekBack / peekForward
// Show a preview of where back/forward would take the user
const prev = router.peekBack();
if (prev) {
console.log(`Back goes to: ${prev.name}`);
}
const next = router.peekForward();
if (next) {
console.log(`Forward goes to: ${next.name}`);
}hasVisited / getVisitedRoutes / getRouteVisitCount
// Check if the user has been to a route in this session
if (router.hasVisited("checkout")) {
showResumeCheckoutBanner();
}
// Get all routes visited in this session
const visited = router.getVisitedRoutes();
// => ["home", "users.list", "users.view", "checkout"]
// How many times did the user visit the product page?
const count = router.getRouteVisitCount("products.view");traverseToLast
// Jump directly to the last time the user was on users.list
// (skips intermediate entries — no back/forward stepping)
await router.traverseToLast("users.list");canGoBack / canGoForward / canGoBackTo
// Disable back button when there's nowhere to go
const backDisabled = !router.canGoBack();
const forwardDisabled = !router.canGoForward();
// Show "back to list" only if the user actually came from the list
if (router.canGoBackTo("users.list")) {
showBackToListButton();
}Navigation Metadata
Navigation metadata is available on state.context.navigation after each transition. The plugin writes it via the claim-based State Context API, and it is frozen (Object.freeze) for mutation protection.
// In subscribe callbacks
router.subscribe((state) => {
const meta = state.context.navigation;
console.log(meta?.navigationType); // "push" | "replace" | "traverse" | "reload"
console.log(meta?.userInitiated); // true if user clicked back/forward/link
console.log(meta?.direction); // "forward" | "back" | "unknown"
console.log(meta?.sourceElement); // the DOM element that initiated the nav, or null
console.log(meta?.info); // data passed via navigation.navigate({ info })
});In guards during browser-initiated navigation, meta is available on toState.context.navigation (written in onTransitionStart):
import { getLifecycleApi } from "@real-router/core/api";
const lifecycle = getLifecycleApi(router);
lifecycle.addActivateGuard("checkout", () => (toState) => {
const meta = toState.context.navigation;
if (meta?.userInitiated) {
// user clicked back/forward or a link
}
return true;
});In framework components, access via the route's context:
// React example
const { route } = useRoute();
const meta = route.context.navigation;NavigationMeta
| Field | Type | Description |
| ----------------- | -------------------------------------------------- | ------------------------------------------------------ |
| navigationType | "push" \| "replace" \| "traverse" \| "reload" | Type of navigation |
| userInitiated | boolean | Whether the user clicked back/forward/link |
| direction | "forward" \| "back" \| "unknown" | Direction in the history stack |
| sourceElement | Element \| null | DOM element that initiated the navigation, or null |
| info | unknown | Ephemeral data from navigation.navigate({ info }) |
NavigationDirection
type NavigationDirection = "forward" | "back" | "unknown";Exported from the package for use in type annotations.
buildUrl vs buildPath
router.buildPath("users", { id: 1 }); // "/users/1" — core, no base
router.buildUrl("users", { id: 1 }); // "/app/users/1" — plugin, with basereplaceHistoryState vs navigate({ replace: true })
router.replaceHistoryState(name, params); // URL only, no transition
router.navigate(name, params, { replace: true }); // Full transition + URL updateFeature Detection
Use navigationPluginFactory when the Navigation API is available, fall back to browserPluginFactory otherwise:
import { browserPluginFactory } from "@real-router/browser-plugin";
import { navigationPluginFactory } from "@real-router/navigation-plugin";
const plugin =
"navigation" in globalThis
? navigationPluginFactory({ base })
: browserPluginFactory({ base });
router.usePlugin(plugin);Form Protection
canDeactivate guards run on browser back/forward by default — no extra configuration needed:
router.usePlugin(navigationPluginFactory());
import { getLifecycleApi } from "@real-router/core/api";
const lifecycle = getLifecycleApi(router);
lifecycle.addDeactivateGuard(
"checkout",
(router, getDep) => (toState, fromState) => {
return !hasUnsavedChanges(); // false blocks back/forward
},
);SSR Support
The plugin is SSR-safe. In a non-browser environment it falls back to no-ops via createNavigationFallbackBrowser:
// Server-side — no errors, methods return safe defaults
router.usePlugin(navigationPluginFactory());
router.buildUrl("home"); // returns path without base
router.matchUrl("/home"); // returns State — matchUrl is pure and works in SSR
router.matchUrl("/does-not-exist"); // returns undefined when no route matchesmatchUrl is environment-agnostic — it delegates to api.matchPath() over the route tree. What the SSR fallback disables is the browser-side side effects: navigate, replaceState, traverseTo, addNavigateListener all become no-ops (with a one-time warn). buildUrl / matchUrl remain fully functional.
Documentation
Full documentation: Wiki — navigation-plugin
Related Packages
| Package | Description |
| ---------------------------------------------------------------------------------------- | ---------------------------------------------- |
| @real-router/core | Core router (required peer dependency) |
| @real-router/browser-plugin | History API fallback (broader browser support) |
| @real-router/hash-plugin | Hash-based routing (#/path) |
| @real-router/react | React integration |
| @real-router/logger-plugin | Development logging |
Contributing
See contributing guidelines for development setup and PR process.
