Skip to content

@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

Terminal window
npm install @xmachines/play-dom-router @xmachines/play-router @xmachines/play-actor

Peer dependency:

Terminal window
npm install xstate@^5.31.0

Overview

@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) drives history.push() — actor is authoritative.
  • Browser navigation events (popstate, pushState, replaceState) send play.route intents back to the actor.
  • Guarded state transitions remain actor-owned (Actor Authority).
  • Circular update prevention via isProcessingNavigation flag — inherited from RouterBridgeBase.

Key Exports

ExportDescription
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
createRouteMapRe-exported from @xmachines/play-router — builds bidirectional path ↔ state ID map
BrowserHistoryInterface for the history wrapper
BrowserWindowStructural window interface (accepts Window, JSDOM, or any test double)
VanillaRouterInterface for the router wrapper
ConnectRouterOptionsOptions type for connectRouter
RouteLookupContractStructural interface for bidirectional route lookup
RoutableActorMinimal actor interface from @xmachines/play-routercurrentRoute, initialRoute, and send(PlayRouteEvent)
RouterBridge, PlayRouteEventTypes re-exported from @xmachines/play-router
RouteMap, RouteMapping, RouteMapOptionsTypes 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 machine
const 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 router
const router = createRouter({ routeTree, history });
// 4. Start actor and connect
const 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 changes
const unsubscribe = history.subscribe((location) => {
console.log("URL changed:", location.pathname, location.search);
});
// Programmatic navigation
history.push("/dashboard");
history.replace("/login");
history.back();
// Cleanup — safe to call more than once; cooperates with other wrappers on the same window
unsubscribe();
history.destroy();

BrowserHistory interface:

MethodDescription
locationRead-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.route events.
  • 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:

OptionTypeDescription
actorRoutableActorActor to synchronize with the browser URL
routerVanillaRouterRouter from createRouter()
routeMapRouteLookupContractBidirectional 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:

Terminal window
npm test
# or from monorepo root:
npm test -w @xmachines/play-dom-router

Tests 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:

TypeThreshold
Lines80%
Functions80%
Branches75%
Statements80%

Architecture

The bridge-first data flow:

  1. connectRouter instantiates DomRouterBridge (extends RouterBridgeBase) and calls bridge.connect().
  2. On connect, RouterBridgeBase performs initial sync: if browser URL differs from actor route, a play.route event is sent; if actor route differs and browser is at the machine’s initial route (restore scenario), the actor wins and history is updated.
  3. Actor route changes (via currentRoute Signal) trigger history.push(path).
  4. Browser URL changes (popstate, patched pushState/replaceState) call syncActorFromRouter(pathname, search), which sends a play.route event.
  5. Circular updates are prevented by the isProcessingNavigation flag in RouterBridgeBase.
Browser URL
│ popstate / pushState / replaceState
DomRouterBridge
│ play.route event
Actor (XState machine)
│ currentRoute Signal change
DomRouterBridge
│ history.push(path)
Browser URL

Part of the XMachines monorepo

This package is part of the xmachines-js monorepo.

License

MIT — see LICENSE.

Interfaces

Functions