@xmachines/play-solid-router
API / @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-router @xmachines/play-routerPeer dependencies. Install them separately:
pnpm add solid-js @solidjs/routersolid-js^1.8.0@solidjs/router^0.16.1or^1.0.0
Quick Start
import { Router, Route, useNavigate, useLocation, useParams } from "@solidjs/router";import { onCleanup, type ParentComponent } from "solid-js";import { PlayRouterProvider } from "@xmachines/play-solid-router";import { createRouteMap } from "@xmachines/play-router/xstate";import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";import { withRouting } from "@xmachines/play-xstate/routing";import { myMachine } from "./machine.js";
// PlayRouterProvider needs the routing capabilityconst actor = definePlayer({ machine: myMachine, actor: compose(PlayerActor, withRouting) })();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 RoutableActor> { /** 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()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) — from @xmachines/play-router/xstate
This factory builds a RouteMap directly from an XState machine definition. This package re-exports it never: the barrel names no state machine.
import { createRouteMap } from "@xmachines/play-router/xstate";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 |
|---|---|
RoutableActor | currentRoute, initialRoute and send, from @xmachines/play-router. It names no view and no engine: routing implies no rendering. PlayRouterProvider constrains its actor to this shape, and a constraint keeps the concrete type whole, so a renderer callback that reads currentView still reports the exact spec of that actor. |
PlayActor | 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 atom. - 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: {} }A bad pattern is a question of the route map, and not of this bridge. A
RouteMapthat holds one parameterized route compiles it in the CONSTRUCTOR, and it throws anInvalidRoutePatternErrorthere for a pattern that URLPattern refuses. Such a route therefore fails at startup, whatever this bridge does. You install no polyfill:@xmachines/play-routercarriesurlpattern-polyfillas an ordinary dependency and it uses the native API when the runtime has one.
What follows is about the CALLS that a navigation makes, which cost time and which the bridge avoids where the path already answers.
The bridge prefers the path parameters of the reactive useParams() proxy of Solid, so a parameterized route reaches URLPattern for its params never while Solid answers for it.
Two cases still read the params with URLPattern:
- The bridge sits under a
basePath. Solid then matched a route of the host, so its params describe the route of the machine never, and a name that collides carries the value of the host. - Solid reports none of the names that the pattern of the machine declares, which a catch-all route of the host does, and the path fills at least one segment of the pattern.
A path that fills NO optional segment reaches URLPattern never. /settings against /settings/:section? is the bare form of the pattern, so the params are {} and the bridge reads that from the path alone.
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.
Classes
Interfaces
- PlayActor
- PlayRouteEvent
- PlayRouterProviderProps
- RoutableActor
- RouteMapOptions
- RouteMapping
- RouterBridge