@xmachines/play-solid-router
v2.1.1
Published
SolidJS Router adapter for XMachines Universal Player Architecture
Readme
@xmachines/play-solid-router
SolidJS Router adapter for the XMachines Universal Player Architecture. It keeps the state machine routes of a PlayerActor and the browser URL in step, in both directions, through @solidjs/router.
Installation
pnpm add @xmachines/play-solid-routerPeer dependencies. Install them separately:
pnpm add solid-js @solidjs/router xstatesolid-js^1.8.0@solidjs/router^0.16.1xstate^5.31.0
Quick Start
import { Router, Route, useNavigate, useLocation, useParams } from "@solidjs/router";
import { onCleanup, type ParentComponent } from "solid-js";
import { PlayRouterProvider, createRouteMap } from "@xmachines/play-solid-router";
import { definePlayer } from "@xmachines/play-xstate";
import { myMachine } from "./machine.js";
const actor = definePlayer({ machine: myMachine })();
actor.start();
const routeMap = createRouteMap(myMachine);
// Minimal app shell stub — a real app renders PlayUIProvider + PlayRenderer from
// @xmachines/play-solid here (see the workspace-only @xmachines/play-solid-demo Shell)
const MyApp = (props: { actor: typeof actor }) => <main />;
const Layout: ParentComponent = () => {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
onCleanup(() => actor.stop());
return (
<PlayRouterProvider
actor={actor}
routeMap={routeMap}
router={{ navigate, location, params }}
renderer={(a, router) => <MyApp actor={a} />}
/>
);
};
export default function App() {
return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
}API Summary
PlayRouterProvider
This SolidJS component connects a PlayerActor to Solid Router. It creates and connects a SolidRouterBridge on mount. It disconnects the bridge with onCleanup on unmount.
interface PlayRouterProviderProps<TActor extends PlayActor> {
/** The actor to sync with Solid Router. */
actor: TActor;
/** Bidirectional route map for state ID ↔ URL path lookups. */
routeMap: RouteMap;
/**
* The three Solid Router hook results that drive bidirectional sync.
* Must be obtained via useNavigate(), useLocation(), and useParams()
* inside a router context.
*/
router: SolidRouterHooks;
/** Render callback — receives the concrete actor type and router hooks. */
renderer: (actor: TActor, router: SolidRouterHooks) => JSX.Element;
}SolidRouterBridge
The low-level class for a manual integration. It extends RouterBridgeBase from @xmachines/play-router. It uses the Solid createEffect to send each router change to the actor.
Important: call
connect()inside a Solid reactive owner: a component, orcreateRoot. The bridge does not clean up by itself. Calldisconnect()ordispose()yourself, usually inonCleanup().
import { useNavigate, useLocation, useParams } from "@solidjs/router";
import { onCleanup } from "solid-js";
import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
// actor: your started player (see the Quick Start above)
function App() {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
const routeMap = new RouteMap([
{ stateId: "#home", path: "/" },
{ stateId: "#profile", path: "/profile/:userId" },
]);
const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
bridge.connect();
onCleanup(() => bridge.disconnect());
return <div>...</div>;
}createRouteMap(machine)
This factory builds a RouteMap directly from an XState machine definition. It comes from @xmachines/play-router.
import { createRouteMap } from "@xmachines/play-solid-router";
const routeMap = createRouteMap(myMachine);RouteMap / RouteMapping
The bidirectional map between the state IDs and the URL paths. It comes from @xmachines/play-router.
import { RouteMap } from "@xmachines/play-solid-router";
const routeMap = new RouteMap([
{ stateId: "#home", path: "/" },
{ stateId: "#profile", path: "/profile/:userId" },
{ stateId: "#settings", path: "/settings/:section?" },
]);Types
| Export | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| PlayActor | AbstractActor & Routable & Viewable — the canonical actor shape from @xmachines/play-router. PlayRouterProvider requires it, because it renders the current view spec and also keeps the routes in step. |
| RoutableActor | Deprecated alias for PlayActor. Use PlayActor from @xmachines/play-router in new code. |
| AbstractActor | It comes from @xmachines/play-actor. Use it for the type of a renderer callback. |
| SolidRouterHooks | Shape of the router prop: { navigate, location, params } |
| PlayRouterProviderProps | Full props interface for PlayRouterProvider |
| PlayRouteEvent | The event type that the bridge sends to the actor on a URL change (play.route) |
| RouterBridge | The interface that SolidRouterBridge implements |
| RouteMapOptions | The options object for the RouteMap constructor. It comes from @xmachines/play-router. |
Usage Patterns
Protected Routes and Guards
The auth guards are inside the state machine only. Unauthorized content therefore never appears, not even for a moment:
const machineConfig = {
states: {
dashboard: {
meta: { route: "/dashboard" },
always: {
guard: ({ context }) => !context.isAuthenticated,
target: "login",
},
},
},
};A user navigates to /dashboard, and the user is not authenticated:
- Solid Router updates the URL.
- The bridge receives the change and sends
play.routeto the actor. - The actor evaluates the guard. The guard refuses the transition, and the actor moves to
login. - The bridge reads the new actor route (
/login) from the TC39 Signal. - The bridge calls
navigate("/login").
Dynamic Routes with Parameters
const routeMap = new RouteMap([
{ stateId: "#post", path: "/users/:userId/posts/:postId" },
{ stateId: "#settings", path: "/settings/:section?" },
]);
// Params are extracted from Solid's useParams() and forwarded in the play.route event:
// { type: "play.route", to: "#post", params: { userId: "123", postId: "456" }, query: {} }The bridge reads the path parameters from the reactive useParams() proxy of Solid. A parameterized route therefore does not need the URLPattern polyfill.
Testing
Run tests for this package in isolation:
# From the monorepo root
pnpm --filter @xmachines/play-solid-router test
# Or from this package directory
pnpm testBrowser tests (test/browser/**/*.browser.test.ts) run in real Chromium through Playwright. They cover the asynchronous sequences that jsdom cannot reproduce:
pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browserCoverage thresholds: 80% lines, functions, branches, and statements.
Related Packages
- @xmachines/play-router — core router primitives and
RouterBridgeBase - @xmachines/play-tanstack-solid-router — TanStack Solid Router adapter
- @xmachines/play-solid — SolidJS view renderer
- @xmachines/play-xstate — XState v5 player factory
Learn More
License
MIT — see LICENSE.
