Multi-Router Integration
How to integrate XMachines Play with the 8 router adapters shipped in this monorepo.
Overview
XMachines provides router adapters for every major JavaScript framework. All adapters implement the same Actor Authority invariant — the state machine guards control navigation, and the router passively observes actor.currentRoute.
There are two integration patterns:
| Pattern | Used by | Key API |
|---|---|---|
| Provider pattern | React (TanStack, React Router), SolidJS (TanStack, SolidJS Router), Vue Router | <PlayRouterProvider actor router routeMap renderer={...} /> |
connectRouter pattern | Vanilla DOM, SvelteKit, Svelte SPA Router | connectRouter({ actor, routeMap }) |
Pattern 1: Provider Pattern
Used by framework adapters that have a React/Solid/Vue provider context. The PlayRouterProvider owns the bridge lifecycle and calls a renderer prop with the current actor and router.
React + TanStack Router (@xmachines/play-tanstack-react-router)
import { useEffect, useMemo } from "react";import { createRouter, createRootRoute } from "@tanstack/react-router";import { PlayRouterProvider, createRouteMapFromTree } from "@xmachines/play-tanstack-react-router";import { extractMachineRoutes } from "@xmachines/play-router";import { definePlayer } from "@xmachines/play-xstate";import { defineRegistry } from "@xmachines/play-react";import { authMachine, authCatalog } from "@xmachines/play-actor-shared";
const createPlayer = definePlayer({ machine: authMachine });
const { registry } = defineRegistry(authCatalog, { components: { /* ...your components */ }, actions: { login: async () => {}, logout: async () => {}, /* ... */ },});
function createAppRuntime() { const actor = createPlayer(); actor.start();
const routeTree = extractMachineRoutes(authMachine); const routeMap = createRouteMapFromTree(routeTree); const router = createRouter({ routeTree: createRootRoute(), defaultPreload: "intent" });
return { actor, routeMap, router };}
export function App() { const { actor, routeMap, router } = useMemo(createAppRuntime, []);
useEffect(() => () => { actor.stop(); }, [actor]);
return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( <Shell actor={currentActor} router={currentRouter} registry={registry} /> )} /> );}React + React Router v7 (@xmachines/play-react-router)
import { createBrowserRouter, RouterProvider } from "react-router";import { PlayRouterProvider, createRouteMapFromTree } from "@xmachines/play-react-router";import { extractMachineRoutes } from "@xmachines/play-router";import { definePlayer } from "@xmachines/play-xstate";
const createPlayer = definePlayer({ machine: authMachine });
function createAppRuntime() { const actor = createPlayer(); actor.start();
const routeTree = extractMachineRoutes(authMachine); const routeMap = createRouteMapFromTree(routeTree);
// React Router requires route elements upfront — use a catch-all let router!: ReturnType<typeof createBrowserRouter>; function RoutedShell() { return <Shell actor={actor} router={router} registry={registry} />; } router = createBrowserRouter([{ path: "*", element: <RoutedShell /> }]);
return { actor, routeMap, router };}
export default function App() { const { actor, routeMap, router } = useMemo(createAppRuntime, []);
useEffect(() => () => { actor.stop(); }, [actor]);
return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( <RouterProvider router={currentRouter} /> )} /> );}SolidJS + SolidJS Router (@xmachines/play-solid-router)
import { Router, Route } from "@solidjs/router";import { onCleanup } from "solid-js";import { PlayRouterProvider, createRouteMap } from "@xmachines/play-solid-router";import { extractMachineRoutes, getRoutableRoutes } from "@xmachines/play-router";import { definePlayer } from "@xmachines/play-xstate";
const createPlayer = definePlayer({ machine: authMachine });const actor = createPlayer();actor.start();
const routeTree = extractMachineRoutes(authMachine);const routes = getRoutableRoutes(routeTree);const routeMap = createRouteMap(authMachine);
// Layout rendered inside Router — has access to navigate/location/params hooksconst Layout = () => { const navigate = useNavigate(); const location = useLocation(); const params = useParams();
onCleanup(() => { actor.stop(); });
return ( <PlayRouterProvider actor={actor} routeMap={routeMap} router={{ navigate, location, params }} renderer={(currentActor, currentRouter) => ( <Shell actor={currentActor} router={currentRouter} registry={registry} /> )} /> );};
export default function App() { return ( <Router root={Layout}> {routes.map((route) => ( <Route path={route.fullPath} component={() => <div />} /> ))} </Router> );}SolidJS + TanStack Solid Router (@xmachines/play-tanstack-solid-router)
import { createRouter, createRootRoute, createRoute, RouterProvider } from "@tanstack/solid-router";import { onCleanup } from "solid-js";import { PlayRouterProvider, createRouteMap } from "@xmachines/play-tanstack-solid-router";import { extractMachineRoutes, getRoutableRoutes } from "@xmachines/play-router";import { definePlayer } from "@xmachines/play-xstate";
const createPlayer = definePlayer({ machine: authMachine });const actor = createPlayer();actor.start();
const routeTree = extractMachineRoutes(authMachine);const routes = getRoutableRoutes(routeTree);const routeMap = createRouteMap(authMachine);
const rootRoute = createRootRoute({ component: Layout });
// Convert XMachines :param to TanStack $param formatconst tanstackRoutes = routes.map((route) => { const tanstackPath = route.fullPath.replace(/:(\w+)/g, "$$$1"); return createRoute({ getParentRoute: () => rootRoute, path: tanstackPath, component: () => <div /> });});
const router = createRouter({ routeTree: rootRoute.addChildren(tanstackRoutes) });
function Layout() { onCleanup(() => { actor.stop(); });
return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( <Shell actor={currentActor} router={currentRouter} registry={registry} /> )} /> );}
export default function App() { return <RouterProvider router={router} />;}Vue + Vue Router (@xmachines/play-vue-router)
<template> <PlayRouterProvider :actor="actor" :route-map="routeMap" :router="router" :renderer="renderShell" /></template>
<script setup lang="ts"> import { h, inject } from "vue"; import { createRouter, createWebHistory } from "vue-router"; import { PlayRouterProvider, createRouteMap } from "@xmachines/play-vue-router"; import { defineRegistry } from "@xmachines/play-vue"; import { authMachine, authCatalog } from "@xmachines/play-actor-shared";
const actor = inject("actor");
const routeMap = createRouteMap(authMachine);
const router = createRouter({ history: createWebHistory(), routes: [ { path: "/:pathMatch(.*)*", name: "xmachines-play", component: { render: () => h("div") }, }, ], });
const { registry } = defineRegistry(authCatalog, { components: { /* ...your Vue SFC components */ }, actions: { login: async () => {}, logout: async () => {} /* ... */ }, });
const renderShell = (currentActor, currentRouter) => h(Shell, { actor: currentActor, router: currentRouter, registry });</script>Pattern 2: connectRouter Pattern
Used by vanilla DOM and Svelte adapters. Call connectRouter directly after creating the actor — no provider component needed.
Vanilla DOM (@xmachines/play-dom-router)
import { createBrowserHistory, createRouter, connectRouter, createRouteMap,} from "@xmachines/play-dom-router";import { extractMachineRoutes } from "@xmachines/play-router";import { definePlayer } from "@xmachines/play-xstate";import { createPlayUI, defineRegistry } from "@xmachines/play-dom";import { authMachine, authCatalog } from "@xmachines/play-actor-shared";
const createPlayer = definePlayer({ machine: authMachine });const actor = createPlayer();actor.start();
// Router setupconst routeTree = extractMachineRoutes(authMachine);const routeMap = createRouteMap(authMachine);const history = createBrowserHistory({ window });const router = createRouter({ routeTree, history });
// connectRouter handles bidirectional sync:// - actor.currentRoute changes → browser URL updated// - browser URL changes → play.route event sent to actorconst disconnect = connectRouter({ actor, router, routeMap });
// Renderer setupconst registryResult = defineRegistry(authCatalog, { components: { /* ...your DOM components */ },});const mount = createPlayUI(registryResult);const disconnectRenderer = mount(actor, document.getElementById("app")!);
// Cleanupwindow.addEventListener("beforeunload", () => { disconnect(); router.destroy(); disconnectRenderer(); actor.stop();});SvelteKit (@xmachines/play-sveltekit-router)
import { defineRegistry } from "@xmachines/play-svelte";import { authCatalog, authMachine } from "@xmachines/play-actor-shared";import { definePlayer } from "@xmachines/play-xstate";import { connectRouter, createRouteMap } from "@xmachines/play-sveltekit-router";
const createDemoPlayer = definePlayer({ machine: authMachine });
const { registry } = defineRegistry(authCatalog, { components: { /* ...your Svelte components */ }, actions: { login: async () => {}, logout: async () => {} /* ... */ },});
export const actor = createDemoPlayer();actor.start();
export const routeMap = createRouteMap(authMachine);export const disconnectRouter = connectRouter({ actor, routeMap });<script lang="ts"> import Shell from "./Shell.svelte"; import { actor, registry } from "./lib/router.js";</script>
<Shell {actor} {registry} />Svelte SPA Router (@xmachines/play-svelte-spa-router)
// lib/router.ts — identical pattern to SvelteKit, different importimport { connectRouter, createRouteMap } from "@xmachines/play-svelte-spa-router";import { definePlayer } from "@xmachines/play-xstate";import { authMachine } from "@xmachines/play-actor-shared";
const createDemoPlayer = definePlayer({ machine: authMachine });
export const actor = createDemoPlayer();actor.start();
export const routeMap = createRouteMap(authMachine);export const disconnectRouter = connectRouter({ actor, routeMap });Adapter Summary
| Package | Framework | Pattern | Key Import |
|---|---|---|---|
@xmachines/play-dom-router | Vanilla DOM | connectRouter | connectRouter, createRouteMap, createBrowserHistory, createRouter |
@xmachines/play-react-router | React Router v7 | Provider | PlayRouterProvider, createRouteMapFromTree |
@xmachines/play-tanstack-react-router | TanStack React Router | Provider | PlayRouterProvider, createRouteMapFromTree |
@xmachines/play-solid-router | SolidJS Router | Provider | PlayRouterProvider, createRouteMap |
@xmachines/play-tanstack-solid-router | TanStack Solid Router | Provider | PlayRouterProvider, createRouteMap |
@xmachines/play-vue-router | Vue Router | Provider | PlayRouterProvider, createRouteMap |
@xmachines/play-sveltekit-router | SvelteKit | connectRouter | connectRouter, createRouteMap |
@xmachines/play-svelte-spa-router | Svelte SPA Router | connectRouter | connectRouter, createRouteMap |
Renderer Packages
Each framework also has a companion renderer package for the view layer:
| Package | Framework | Key API |
|---|---|---|
@xmachines/play-dom | Vanilla DOM | createPlayUI, createRenderer, defineRegistry |
@xmachines/play-react | React | PlayRenderer, defineRegistry |
@xmachines/play-solid | SolidJS | PlayRenderer, defineRegistry |
@xmachines/play-vue | Vue 3 | PlayRenderer, defineRegistry |
@xmachines/play-svelte | Svelte 5 | PlayRenderer, defineRegistry |
Architectural Invariants
All adapters preserve the 5 architectural invariants:
- Actor Authority (INV-01): Machine guards validate all navigation
- Strict Separation (INV-02): Business logic (machine) has zero framework imports
- State-Driven Reset (INV-03): Browser back/forward sends
play.routeevents to actor - Passive Infrastructure (INV-04): Router observes actor, never decides
- Signal-Only Reactivity (INV-05): TC39 Signals work identically in all adapters
Next Steps
- Routing Patterns —
meta.route,play.route,alwaysguards - Examples Index — Complete catalog of runnable demos
Learn More
- Play RFC — Complete architectural specification
- play-router — Route extraction and route maps
- play-dom-router — Vanilla DOM router adapter