Skip to content

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:

PatternUsed byKey API
Provider patternReact (TanStack, React Router), SolidJS (TanStack, SolidJS Router), Vue Router<PlayRouterProvider actor router routeMap renderer={...} />
connectRouter patternVanilla DOM, SvelteKit, Svelte SPA RouterconnectRouter({ 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 hooks
const 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 format
const 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)

App.vue
<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 setup
const 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 actor
const disconnect = connectRouter({ actor, router, routeMap });
// Renderer setup
const registryResult = defineRegistry(authCatalog, {
components: {
/* ...your DOM components */
},
});
const mount = createPlayUI(registryResult);
const disconnectRenderer = mount(actor, document.getElementById("app")!);
// Cleanup
window.addEventListener("beforeunload", () => {
disconnect();
router.destroy();
disconnectRenderer();
actor.stop();
});

SvelteKit (@xmachines/play-sveltekit-router)

lib/router.ts
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 });
App.svelte
<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 import
import { 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

PackageFrameworkPatternKey Import
@xmachines/play-dom-routerVanilla DOMconnectRouterconnectRouter, createRouteMap, createBrowserHistory, createRouter
@xmachines/play-react-routerReact Router v7ProviderPlayRouterProvider, createRouteMapFromTree
@xmachines/play-tanstack-react-routerTanStack React RouterProviderPlayRouterProvider, createRouteMapFromTree
@xmachines/play-solid-routerSolidJS RouterProviderPlayRouterProvider, createRouteMap
@xmachines/play-tanstack-solid-routerTanStack Solid RouterProviderPlayRouterProvider, createRouteMap
@xmachines/play-vue-routerVue RouterProviderPlayRouterProvider, createRouteMap
@xmachines/play-sveltekit-routerSvelteKitconnectRouterconnectRouter, createRouteMap
@xmachines/play-svelte-spa-routerSvelte SPA RouterconnectRouterconnectRouter, createRouteMap

Renderer Packages

Each framework also has a companion renderer package for the view layer:

PackageFrameworkKey API
@xmachines/play-domVanilla DOMcreatePlayUI, createRenderer, defineRegistry
@xmachines/play-reactReactPlayRenderer, defineRegistry
@xmachines/play-solidSolidJSPlayRenderer, defineRegistry
@xmachines/play-vueVue 3PlayRenderer, defineRegistry
@xmachines/play-svelteSvelte 5PlayRenderer, defineRegistry

Architectural Invariants

All adapters preserve the 5 architectural invariants:

  1. Actor Authority (INV-01): Machine guards validate all navigation
  2. Strict Separation (INV-02): Business logic (machine) has zero framework imports
  3. State-Driven Reset (INV-03): Browser back/forward sends play.route events to actor
  4. Passive Infrastructure (INV-04): Router observes actor, never decides
  5. Signal-Only Reactivity (INV-05): TC39 Signals work identically in all adapters

Next Steps

Learn More