@xmachines/play-dom-router
API / @xmachines/play-dom-router
Vanilla DOM router (Browser History API) for XMachines Play Architecture.
Framework-agnostic router integration that synchronizes a Play actor’s currentRoute TC39 Signal with the browser’s window.history API — no framework required. Implements the same RouterBridgeBase pattern as all other router adapters in the XMachines ecosystem.
Installation
npm install @xmachines/play-dom-router @xmachines/play-router @xmachines/play-actorPeer dependency:
npm install xstate@^5.31.0Overview
@xmachines/play-dom-router connects a Play actor to the browser URL through the DomRouterBridge (extends RouterBridgeBase from @xmachines/play-router):
- Actor route signal (
actor.currentRoute) driveshistory.push()— actor is authoritative. - Browser navigation events (
popstate,pushState,replaceState) sendplay.routeintents back to the actor. - Guarded state transitions remain actor-owned (Actor Authority).
- Circular update prevention via
isProcessingNavigationflag — inherited fromRouterBridgeBase.
Key Exports
| Export | Description |
|---|---|
createBrowserHistory(options) | Wraps window.history with a subscribable BrowserHistory interface |
createRouter(options) | Creates a VanillaRouter combining BrowserHistory and RouteTree |
connectRouter(options) | Connects a VanillaRouter to a Routable actor — returns a disconnect cleanup function |
createRouteMap | Re-exported from @xmachines/play-router — builds bidirectional path ↔ state ID map |
BrowserHistory | Interface for the history wrapper |
BrowserWindow | Structural window interface (accepts Window, JSDOM, or any test double) |
VanillaRouter | Interface for the router wrapper |
ConnectRouterOptions | Options type for connectRouter |
RouteLookupContract | Structural interface for bidirectional route lookup |
RoutableActor | Minimal actor interface from @xmachines/play-router — currentRoute, initialRoute, and send(PlayRouteEvent) |
RouterBridge, PlayRouteEvent | Types re-exported from @xmachines/play-router |
RouteMap, RouteMapping, RouteMapOptions | Types re-exported from @xmachines/play-router |
Quick Start
import { createBrowserHistory, createRouter, connectRouter, createRouteMap,} from "@xmachines/play-dom-router";import { extractMachineRoutes } from "@xmachines/play-router";
// 1. Extract route tree and build route map from your XState machineconst routeTree = extractMachineRoutes(machine);const routeMap = createRouteMap(machine);
// 2. Create browser history wrapper (accepts window or any BrowserWindow-compatible object)const history = createBrowserHistory({ window });
// 3. Create routerconst router = createRouter({ routeTree, history });
// 4. Start actor and connectconst actor = definePlayer({ machine, catalog })();actor.start();
const disconnect = connectRouter({ actor, router, routeMap });
// Cleanup (e.g. on page unload)window.addEventListener("beforeunload", () => { disconnect(); router.destroy();});API
createBrowserHistory(options)
Wraps window.history to provide a subscribable history interface. Patches pushState/replaceState to detect programmatic navigations in addition to popstate events for back/forward.
const history = createBrowserHistory({ window });
// Subscribe to URL changesconst unsubscribe = history.subscribe((location) => { console.log("URL changed:", location.pathname, location.search);});
// Programmatic navigationhistory.push("/dashboard");history.replace("/login");history.back();
// Cleanup — safe to call more than once; cooperates with other wrappers on the same windowunsubscribe();history.destroy();BrowserHistory interface:
| Method | Description |
|---|---|
location | Read-only { pathname, search, hash, state } |
push(path, state?) | Push a new entry to history |
replace(path, state?) | Replace the current history entry |
go(delta) | Navigate relative to current position |
back() | Navigate backward |
forward() | Navigate forward |
subscribe(listener) | Subscribe to location changes — returns unsubscribe function |
createHref(path) | Create an href from a path |
destroy() | Cleanup — removes listeners and restores patched methods when last wrapper |
BrowserWindow interface:
Accepts window, a JSDOM window, or any object implementing the structural interface. Covers only properties actually used — avoids coupling to Window & typeof globalThis.
createRouter(options)
Creates a VanillaRouter wrapping history and routeTree. Designed for parity with TanStack Router’s setup flow.
const router = createRouter({ routeTree, history });// router.history — the BrowserHistory instance// router.routeTree — for structure reference// router.destroy() — calls history.destroy()connectRouter(options)
Connects a VanillaRouter to a Routable actor. Handles all bidirectional synchronization:
- On connect: syncs initial URL → actor or actor route → browser (restore vs. deep-link detection).
- While connected: actor route changes push to history; browser navigations send
play.routeevents. - Returns a cleanup function that disconnects the bridge.
const disconnect = connectRouter({ actor, // RoutableActor — any AbstractActor subclass satisfies this structurally router, // VanillaRouter from createRouter() routeMap, // RouteLookupContract — any object with getStateIdByPath / getPathByStateId});
// Later:disconnect();ConnectRouterOptions:
| Option | Type | Description |
|---|---|---|
actor | RoutableActor | Actor to synchronize with the browser URL |
router | VanillaRouter | Router from createRouter() |
routeMap | RouteLookupContract | Bidirectional path ↔ state ID lookup |
RouteLookupContract:
interface RouteLookupContract { getStateIdByPath(path: string): string | null | undefined; getPathByStateId(id: string): string | null | undefined;}Any object satisfying this structural interface is accepted — including RouteMap instances from @xmachines/play-router, subclasses, or test doubles.
createRouteMap (re-export)
Re-exported from @xmachines/play-router. Builds a bidirectional RouteMap from an XState machine:
import { createRouteMap } from "@xmachines/play-dom-router";
const routeMap = createRouteMap(machine);routeMap.getStateIdByPath("/dashboard"); // "dashboard"routeMap.getPathByStateId("dashboard"); // "/dashboard"URLPattern Support
This package uses the URLPattern API for route pattern matching via @xmachines/play-router. URLPattern is available natively on Node.js 24+ and modern browsers (Chrome 95+, Firefox 117+, Safari 16.4+). On older environments, load a polyfill before importing this package — see @xmachines/play-router for details.
Testing
Run tests in isolation:
npm test# or from monorepo root:npm test -w @xmachines/play-dom-routerTests run in Node.js environment with URLPattern polyfill setup. Browser-specific tests are in test/browser/ and run separately via vitest.browser.config.ts.
Coverage thresholds:
| Type | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 75% |
| Statements | 80% |
Architecture
The bridge-first data flow:
connectRouterinstantiatesDomRouterBridge(extendsRouterBridgeBase) and callsbridge.connect().- On connect,
RouterBridgeBaseperforms initial sync: if browser URL differs from actor route, aplay.routeevent is sent; if actor route differs and browser is at the machine’s initial route (restore scenario), the actor wins and history is updated. - Actor route changes (via
currentRouteSignal) triggerhistory.push(path). - Browser URL changes (popstate, patched pushState/replaceState) call
syncActorFromRouter(pathname, search), which sends aplay.routeevent. - Circular updates are prevented by the
isProcessingNavigationflag inRouterBridgeBase.
Browser URL │ popstate / pushState / replaceState ▼DomRouterBridge │ play.route event ▼Actor (XState machine) │ currentRoute Signal change ▼DomRouterBridge │ history.push(path) ▼Browser URLRelated Packages
- @xmachines/play-router —
RouterBridgeBase,createRouteMap,extractMachineRoutes - @xmachines/play-actor —
AbstractActor,Routable,Viewable; all subclasses satisfyRoutableActorstructurally - @xmachines/play-dom — Vanilla DOM renderer for view rendering alongside routing
- @xmachines/play-xstate —
definePlayer,PlayerActor
Part of the XMachines monorepo
This package is part of the xmachines-js monorepo.
License
MIT — see LICENSE.
Interfaces
- BrowserHistory
- BrowserWindow
- ConnectRouterOptions
- PlayRouteEvent
- RoutableActor
- RouteLookupContract
- RouteMap
- RouteMapOptions
- RouteMapping
- RouterBridge
- VanillaRouter