@xmachines/play-tanstack-solid-router
API / @xmachines/play-tanstack-solid-router
TanStack Solid Router adapter for XMachines Universal Player Architecture
Signals-native integration with TanStack Solid Router enabling logic-driven navigation through Solid.js reactivity.
Part of the xmachines-js monorepo.
Overview
@xmachines/play-tanstack-solid-router connects a Play actor to TanStack Solid Router through TanStackSolidRouterBridge.
The bridge extends RouterBridgeBase from @xmachines/play-router, keeping adapter behavior consistent across frameworks:
- Actor route signal (
actor.currentRoute) drives router navigation. - Router history events send
play.routeintents back to the actor. - Guarded state transitions remain actor-owned (Actor Authority).
- Circular update prevention built into
RouterBridgeBase.
Installation
npm install @tanstack/solid-router solid-jsnpm install @xmachines/play-tanstack-solid-router @xmachines/play-routerPeer dependencies:
@tanstack/solid-router^1.168.7solid-js^1.8.0xstate^5.31.0
Current Exports
TanStackSolidRouterBridge— primary adapter classPlayRouterProvider— Solid component wrapper for bridge lifecyclePlayRouterProviderProps,TanStackRouterInstance(types)PlayActor— canonical actor shape (AbstractActor & Routable & Viewable) from@xmachines/play-router; use when constructing a typed renderer callback forPlayRouterProviderRoutableActor— deprecated alias forPlayActor; usePlayActorfrom@xmachines/play-routerinsteadRouteMap,createRouteMap,RouteMapping,RouteMapOptions(re-exported from@xmachines/play-router)TanStackRouterLike(type)RouterBridge,PlayRouteEvent(types)
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.
Quick Start
import { createRouter } from "@tanstack/solid-router";import { definePlayer } from "@xmachines/play-xstate";import { extractMachineRoutes } from "@xmachines/play-router";import { TanStackSolidRouterBridge, createRouteMap } from "@xmachines/play-tanstack-solid-router";
const routeMap = createRouteMap(machine);const router = createRouter({ routeTree: tanstackRouteTree });
const actor = definePlayer({ machine })();actor.start();
const bridge = new TanStackSolidRouterBridge(router, actor, routeMap);bridge.connect();
// laterbridge.disconnect();Solid convenience wrapper
Use PlayRouterProvider when you want bridge lifecycle wiring managed by a component:
import { PlayRouterProvider } from "@xmachines/play-tanstack-solid-router";import { RouterProvider } from "@tanstack/solid-router";
<PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( <RouterProvider router={currentRouter}>{/* your app here */}</RouterProvider> )}/>;API
TanStackSolidRouterBridge
Primary adapter class extending RouterBridgeBase.
class TanStackSolidRouterBridge { constructor(router: TanStackRouterLike, actor: RoutableActor, routeMap: RouteMap); connect(): void; disconnect(): void; dispose(): void; // alias for disconnect()}Behavior:
connect()— subscribes torouter.history, performs initial deep-link sync fromrouter.history.location, and starts watchingactor.currentRoutefor state-driven navigation.disconnect()— unsubscribes from history and stops all sync.- Navigates via
router.navigate({ to: path }). - Subscribes via
router.history.subscribe— covers PUSH, POP, BACK, FORWARD, REPLACE, and GO, and works without<RouterProvider>mounted.
PlayRouterProvider
Solid component that instantiates, connects, and cleans up a TanStackSolidRouterBridge automatically.
interface PlayRouterProviderProps<TActor extends PlayActor = PlayActor> { /** The actor to sync with TanStack Solid Router. */ actor: TActor; /** The TanStack Router instance returned by `createRouter`. */ router: TanStackRouterInstance; /** Bidirectional route map for state ID ↔ URL path lookups. */ routeMap: RouteMap; /** Renderer callback receives the same concrete actor type that was passed in. */ renderer: (actor: TActor, router: TanStackRouterInstance) => JSX.Element;}The bridge is created synchronously at component evaluation time (Solid’s execution model) and torn down via onCleanup when the component is disposed.
RouteMap and createRouteMap
Map state IDs to URL paths and resolve URLs back to state IDs.
const routeMap = new RouteMap([ { stateId: "home", path: "/" }, { stateId: "profile", path: "/profile/:userId" }, { stateId: "settings", path: "/settings/:section?" },]);
routeMap.getStateIdByPath("/profile/123"); // "profile"routeMap.getPathByStateId("home"); // "/"routeMap.getStateIdByPath("/unknown"); // null
// Or build from a machine directly:import { createRouteMap } from "@xmachines/play-tanstack-solid-router";const routeMap = createRouteMap(machine);getStateIdByPath returns null (not undefined) for unmatched paths.
Usage Patterns
Dynamic Routes with Parameters
const routeMap = new RouteMap([ { stateId: "post", path: "/users/:userId/posts/:postId" }, { stateId: "settings", path: "/settings/:section?" },]);
// Params are extracted and forwarded in the play.route event:// { type: "play.route", to: "#post", params: { userId: "123", postId: "456" }, query: {} }Protected Routes and Guards
Auth guards live entirely inside the state machine, preventing flashes of unauthorized content:
const machineConfig = { states: { dashboard: { meta: { route: "/dashboard" }, always: { guard: ({ context }) => !context.isAuthenticated, target: "login", }, }, },};When a user navigates to /dashboard while unauthenticated:
- TanStack Router updates location.
- Bridge intercepts and sends
play.routeto the actor. - Actor evaluates the guard — denies transition, moves to
logininstead. - Bridge observes new actor route (
/login). - Bridge calls
router.navigate({ to: "/login" }).
Full App Example
import { createRouter, RouterProvider, createRootRoute, createRoute } from "@tanstack/solid-router";import { onCleanup } from "solid-js";import { PlayRouterProvider, createRouteMap } from "@xmachines/play-tanstack-solid-router";import { definePlayer } from "@xmachines/play-xstate";import { extractMachineRoutes, getRoutableRoutes } from "@xmachines/play-router";
const createPlayer = definePlayer({ machine: authMachine });const actor = createPlayer();actor.start();
const routeMap = createRouteMap(authMachine);const routeTree = extractMachineRoutes(authMachine);const routes = getRoutableRoutes(routeTree);
const rootRoute = createRootRoute({ component: () => { onCleanup(() => actor.stop()); return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( /* your shell/renderer here */ <div /> )} /> ); },});
const tanstackRoutes = routes.map((route) => createRoute({ getParentRoute: () => rootRoute, path: route.fullPath.replace(/:(\w+)/g, "$$$1"), component: () => null, }),);
export const router = createRouter({ routeTree: rootRoute.addChildren(tanstackRoutes) });
export default function App() { return <RouterProvider router={router} />;}Architecture
Bridge-first data flow:
RouterBridgeBase.connect()performs initial actor/router synchronization — both pathname and query string fromrouter.history.locationare forwarded to the actor on first connect.- Actor route updates (
actor.currentRoutesignal) call TanStack navigation (router.navigate({ to })). - TanStack history updates are subscribed and translated to
play.routeevents sent to the actor. - Actor guards accept or reject transitions; infrastructure reflects resulting state.
This keeps routing infrastructure passive while preserving business-logic control in the state machine.
Testing
Run tests for this package in isolation:
npm test -w packages/play-tanstack-solid-routerOr from the package directory:
npm testBrowser tests (test/browser/**/*.browser.test.ts) run against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce: BACK/FORWARD navigation via router.history.subscribe, echo suppression under real microtask timing, navigate({ to }) call verification, and subscriber teardown on disconnect() and dispose().
# Run browser tests onlynpx vitest --config vitest.browser.config.ts --project play-tanstack-solid-router-browserCoverage thresholds: lines 80%, functions 80%, branches 70%, statements 80%.
Related Packages
- @xmachines/play-router — core router primitives and
RouterBridgeBase - @xmachines/play-tanstack-react-router — TanStack Router (React) equivalent
- @xmachines/play-solid — SolidJS renderer
- @xmachines/play-solid-router — native
@solidjs/routeradapter - @xmachines/play-xstate — XState v5 player factory
Learn More
License
MIT — see LICENSE.