@real-router/browser-plugin
v0.17.1
Published
Browser integration plugin with History API, hash routing, and popstate support
Maintainers
Readme
@real-router/browser-plugin
Browser History API integration for Real-Router. Synchronizes router state with browser URL and handles back/forward navigation.
Installation
npm install @real-router/browser-pluginPeer dependency: @real-router/core
Quick Start
import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";
const router = createRouter([
{ name: "home", path: "/" },
{ name: "users", path: "/users/:id" },
]);
router.usePlugin(browserPluginFactory());
await router.start(); // path inferred from browser locationOptions
router.usePlugin(
browserPluginFactory({
base: "/app", // Base path prefix for all routes
forceDeactivate: true, // Bypass canDeactivate guards on back/forward
}),
);| Option | Type | Default | Description |
| ----------------- | --------- | ------- | ---------------------------------------------------------------------- |
| base | string | "" | Base path for all routes (e.g., "/app" → URLs start with /app/...) |
| forceDeactivate | boolean | true | Bypass canDeactivate guards on browser back/forward |
Hash routing? Use
@real-router/hash-plugininstead.
Router Extensions
The plugin extends the router instance with three methods via extendRouter():
| Method | Returns | Description |
| ----------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------- |
| buildUrl(name, params?, options?: { hash? }) | string | Build full URL with base path. options.hash (decoded) is encoded and appended. |
| matchUrl(url) | State \| undefined | Parse URL to router state |
| replaceHistoryState(name, params?, options?: { hash? }) | void | Update browser URL without triggering navigation. Tri-state hash: undefined preserves, "" clears, value sets. |
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" });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 updateURL Fragment ("hash") Support
browser-plugin ships first-class URL-fragment support: <Link hash> from any framework adapter, programmatic router.navigate(name, params, { hash }), and a state.context.url = { hash, hashChanged } namespace populated on every transition.
// Programmatic — tri-state opts.hash
router.navigate("settings", {}, { hash: "profile" }); // set
router.navigate("settings", {}, { hash: "" }); // clear
router.navigate("settings"); // preserve current
// In subscribers
router.subscribe(({ route }) => {
console.log(route.context.url?.hash); // "profile"
console.log(route.context.url?.hashChanged); // true on hash-only nav
});See the Hash Fragment Support wiki page for the full surface (encoding, F5 priming, hash-aware active state).
Navigation Source (state.context.browser.source)
On every successful transition the plugin tags state.context.browser with the trigger origin — use this in subscribe handlers to distinguish back/forward from programmatic navigation:
router.subscribe(({ route }) => {
if (route.context.browser?.source === "popstate") {
// back/forward button — restore scroll, skip analytics "view" event, ...
} else {
// router.navigate()/router.start() — programmatic
}
});Both values are frozen singletons, so object-identity comparison is safe and zero-allocation.
| Value | Meaning |
| ----------- | ------------------------------------------------------------- |
| "navigate" | Triggered by router.navigate(), router.start(), or replaceHistoryState() |
| "popstate" | Triggered by browser back/forward buttons (popstate event) |
Form Protection
Set forceDeactivate: false to respect canDeactivate guards on back/forward:
router.usePlugin(browserPluginFactory({ forceDeactivate: false }));
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 — createSafeBrowser detects the absence of window/history and swaps the History API calls (pushState, replaceState, addPopstateListener, getLocation, getHash) for warn-once no-ops. Pure URL helpers are environment-agnostic and behave identically on the server:
// Server-side — no crashes, History API calls become no-ops
router.usePlugin(browserPluginFactory({ base: "/app" }));
router.buildUrl("home"); // "/app/home" — base is still applied
router.matchUrl("/app/users/123"); // { name: "users", params: { id: "123" }, path: "/users/123" }
await router.start("/app/home"); // transitions normally; no popstate subscriptionThe first call to any History API method emits a one-time console warning so accidental SSR usage is visible without spamming logs.
Documentation
Full documentation: Wiki — browser-plugin
Related Packages
| Package | Description |
| -------------------------------------------------------------------------------------- | -------------------------------------- |
| @real-router/core | Core router (required peer dependency) |
| @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.
