Skip to content

@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.route intents back to the actor.
  • Guarded state transitions remain actor-owned (Actor Authority).
  • Circular update prevention built into RouterBridgeBase.

Installation

Terminal window
npm install @tanstack/solid-router solid-js
npm install @xmachines/play-tanstack-solid-router @xmachines/play-router

Peer dependencies:

  • @tanstack/solid-router ^1.168.7
  • solid-js ^1.8.0
  • xstate ^5.31.0

Current Exports

  • TanStackSolidRouterBridge — primary adapter class
  • PlayRouterProvider — Solid component wrapper for bridge lifecycle
  • PlayRouterProviderProps, TanStackRouterInstance (types)
  • PlayActor — canonical actor shape (AbstractActor & Routable & Viewable) from @xmachines/play-router; use when constructing a typed renderer callback for PlayRouterProvider
  • RoutableActor — deprecated alias for PlayActor; use PlayActor from @xmachines/play-router instead
  • RouteMap, 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();
// later
bridge.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 to router.history, performs initial deep-link sync from router.history.location, and starts watching actor.currentRoute for 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:

  1. TanStack Router updates location.
  2. Bridge intercepts and sends play.route to the actor.
  3. Actor evaluates the guard — denies transition, moves to login instead.
  4. Bridge observes new actor route (/login).
  5. 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:

  1. RouterBridgeBase.connect() performs initial actor/router synchronization — both pathname and query string from router.history.location are forwarded to the actor on first connect.
  2. Actor route updates (actor.currentRoute signal) call TanStack navigation (router.navigate({ to })).
  3. TanStack history updates are subscribed and translated to play.route events sent to the actor.
  4. 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:

Terminal window
npm test -w packages/play-tanstack-solid-router

Or from the package directory:

Terminal window
npm test

Browser 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().

Terminal window
# Run browser tests only
npx vitest --config vitest.browser.config.ts --project play-tanstack-solid-router-browser

Coverage thresholds: lines 80%, functions 80%, branches 70%, statements 80%.

Learn More

License

MIT — see LICENSE.

Classes

Interfaces

Type Aliases

Functions