Skip to content

@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.

License: MIT Version

Installation

Terminal window
pnpm add @xmachines/play-solid-router

Peer dependencies. Install them separately:

Terminal window
pnpm add solid-js @solidjs/router xstate
  • solid-js ^1.8.0
  • @solidjs/router ^0.16.1
  • xstate ^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, or createRoot. The bridge does not clean up by itself. Call disconnect() or dispose() yourself, usually in onCleanup().

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

ExportDescription
PlayActorAbstractActor & 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.
RoutableActorDeprecated alias for PlayActor. Use PlayActor from @xmachines/play-router in new code.
AbstractActorIt comes from @xmachines/play-actor. Use it for the type of a renderer callback.
SolidRouterHooksShape of the router prop: { navigate, location, params }
PlayRouterProviderPropsFull props interface for PlayRouterProvider
PlayRouteEventThe event type that the bridge sends to the actor on a URL change (play.route)
RouterBridgeThe interface that SolidRouterBridge implements
RouteMapOptionsThe 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:

  1. Solid Router updates the URL.
  2. The bridge receives the change and sends play.route to the actor.
  3. The actor evaluates the guard. The guard refuses the transition, and the actor moves to login.
  4. The bridge reads the new actor route (/login) from the TC39 Signal.
  5. 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 polyfill is a question of the route map, and not of this bridge. A RouteMap that holds one parameterized route compiles it in the CONSTRUCTOR, and it throws a URLPatternUnavailableError there when the runtime has no URLPattern. An application with such a route therefore needs the polyfill at startup, whatever this bridge does. Only a route map of static paths alone escapes it.

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.

Testing

Run tests for this package in isolation:

Terminal window
# From the monorepo root
pnpm --filter @xmachines/play-solid-router test
# Or from this package directory
pnpm test

Browser tests (test/browser/**/*.browser.test.ts) run in real Chromium through Playwright. They cover the asynchronous sequences that jsdom cannot reproduce:

Terminal window
pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browser

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

Learn More

License

MIT — see LICENSE.

Classes

Interfaces

Type Aliases

Variables

Functions