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 @xmachines/play-router @xmachines/core @xmachines/play @xmachines/play-atom

@xmachines/play-router names @xmachines/core, @xmachines/play and @xmachines/play-atom as peers, so the command installs them with this package.

Peer dependencies. Install them separately:

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

ExportDescription
RoutableActorcurrentRoute, 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.
PlayActorIt 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 atom.
  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: {} }

A bad pattern 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 an InvalidRoutePatternError there for a pattern that URLPattern refuses. Such a route therefore fails at startup, whatever this bridge does. You install no polyfill: @xmachines/play-router carries urlpattern-polyfill as 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.

Learn More

License

MIT — see LICENSE.

Classes

Interfaces

Type Aliases

Variables

Functions