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 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:

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