@xmachines/play-sveltekit-router
API / @xmachines/play-sveltekit-router
SvelteKit router adapter for the XMachines Universal Player Architecture. It keeps the state machine routes of a RoutableActor and the browser URL in step, in both directions, through the $app/navigation module of SvelteKit.
Installation
pnpm add @xmachines/play-sveltekit-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:
pnpm add @sveltejs/kit svelte| Peer dependency | Required version |
|---|---|
@sveltejs/kit | ^2.12.0 || ^3.0.0 |
svelte | ^5.0.0 |
Quick Start
import { onDestroy } from "svelte";import { page } from "$app/state";import { connectRouter } from "@xmachines/play-sveltekit-router";import { createRouteMap } from "@xmachines/play-router/xstate";import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";import { withRouting } from "@xmachines/play-xstate/routing";import type { AnyStateMachine } from "xstate";
declare const myMachine: AnyStateMachine; // your machine, with `meta.route` on its routable states
// 1. Create the actor — `connectRouter` needs the routing capabilityconst createPlayer = definePlayer({ machine: myMachine, actor: compose(PlayerActor, withRouting) });const actor = createPlayer();actor.start();
// 2. Build a bidirectional route map from the machine definitionconst routeMap = createRouteMap(myMachine);
// 3. Connect the router — returns a cleanup function. The server has no// `globalThis.location`, so give it the URL of the request. Without it, the// server renders the initial route of the actor and not the requested page.const disconnect = connectRouter({ actor, routeMap, location: globalThis.location ?? { pathname: page.url.pathname, search: page.url.search },});
// 4. Clean up when the component is destroyedonDestroy(() => disconnect());The bridge works in both directions:
- URL → actor: the bridge converts each SvelteKit
afterNavigateevent into aplay.routeevent, then sends it to the actor. Withshallow: "sync", it converts each URL that the browser shows, the shallow URLs too. See Shallow navigation. - Actor → URL: the bridge writes each change of the actor
currentRouteatom to the browser URL withgoto().
Usage
connectRouter(options) — high-level API
This function is the primary integration point. It creates a SvelteKitRouterBridge, calls connect(), and returns a cleanup function.
import { onDestroy } from "svelte";import { connectRouter, type RoutableActor } from "@xmachines/play-sveltekit-router";import { createRouteMap } from "@xmachines/play-router/xstate";import type { AnyStateMachine } from "xstate";
// machine and actor as in the Quick Start abovedeclare const machine: AnyStateMachine;declare const actor: RoutableActor;
const routeMap = createRouteMap(machine);
// In a Svelte component or SvelteKit layoutconst disconnect = connectRouter({ actor, routeMap });
// Or pass an explicit location for SSR or test environmentsconst disconnectSsr = connectRouter({ actor, routeMap, location: { pathname: "/dashboard", search: "?tab=stats" },});
// CleanuponDestroy(() => disconnect());ConnectRouterOptions:
| Property | Type | Required | Description |
|---|---|---|---|
actor | RoutableActor | Yes | The XMachines actor that the bridge keeps in step with the router |
routeMap | RouteMap | Yes | The bidirectional map between the state IDs and the URL paths |
location | LocationLike | null | No | The location stub for the initial URL reads. The default is globalThis.location. Give a mock in a test, or a stub in an SSR environment. |
shallow | "ignore" | "sync" | No | What the bridge does with a shallow navigation. The default is "ignore". See Shallow navigation. |
Shallow navigation
A shallow navigation changes the URL and loads no page: goto(url, { shallow: true }) in SvelteKit 3, pushState and replaceState, and a back or forward step between shallow entries. A modal that has its own URL is the usual case.
shallow: "ignore"(the default): the machine keeps its route. The bridge watchesafterNavigate.shallow: "sync": the machine follows each URL that the browser shows, the shallow URLs too, back and forward included. The bridge watchespageof$app/state.
afterNavigate cannot serve "sync". SvelteKit 3 calls it for a shallow goto, but it calls it for no back or forward step between shallow entries. SvelteKit 2 calls it for no shallow navigation.
import { onDestroy } from "svelte";import { connectRouter, type RoutableActor } from "@xmachines/play-sveltekit-router";import { createRouteMap } from "@xmachines/play-router/xstate";import type { AnyStateMachine } from "xstate";
// machine and actor as in the Quick Start abovedeclare const machine: AnyStateMachine;declare const actor: RoutableActor;
const disconnect = connectRouter({ actor, routeMap: createRouteMap(machine), shallow: "sync" });onDestroy(() => disconnect());Give each shallow URL a route in the machine. For a URL that the machine does not know, the bridge navigates back to the route of the machine, and that navigation loads the page.
On SvelteKit 2, give pushState and replaceState a NEW state object on each call. SvelteKit 2 changes only page.state for a shallow navigation, and the bridge sees the change of that object. A call with the state object that the page holds already, such as replaceState(url, page.state), changes the URL and nothing that the bridge can observe, so the machine keeps its route. SvelteKit 3 has no such limit.
SvelteKitRouterBridge — low-level class
This class extends RouterBridgeBase from @xmachines/play-router. Use it directly when you need more control over the connection lifecycle.
import { SvelteKitRouterBridge, type RoutableActor } from "@xmachines/play-sveltekit-router";import { createRouteMap } from "@xmachines/play-router/xstate";import type { AnyStateMachine } from "xstate";
// machine and actor as in the Quick Start abovedeclare const machine: AnyStateMachine;declare const actor: RoutableActor;
const routeMap = createRouteMap(machine);const bridge = new SvelteKitRouterBridge(actor, routeMap, location);bridge.connect();
// On teardownbridge.disconnect();SvelteKit integration details:
navigateRouter→goto(path, { reset: false })on SvelteKit 3, andgoto(path, { noScroll: true, keepFocus: true })on SvelteKit 2. The bridge readsVERSIONof@sveltejs/kitto choose the options. Both keep the scroll position and the focused element. On the server, the bridge calls nogoto: SvelteKit has none therewatchRouterChanges→ registers anafterNavigatecallback through an indirection cell, which prevents a direct capture ofthis. The callback ignores a shallow navigation (goto(url, { shallow: true })), because the URL changes and no page loadsunwatchRouterChanges→ sets the indirection cell to null, which releases the bridge reference before the component unmounts (SvelteKit has no API to cancelafterNavigate)
createRouteMap(machine) — from @xmachines/play-router/xstate
This factory builds a RouteMap from an XState machine definition. It reads each route pattern from state.meta.route. This package re-exports it never: the barrel names no state machine, so import the function from @xmachines/play-router/xstate.
import { createRouteMap } from "@xmachines/play-router/xstate";import { createMachine } from "xstate";
const machine = createMachine({ id: "app", initial: "home", states: { home: { meta: { route: "/home" } }, dashboard: { meta: { route: "/dashboard" } }, settings: { meta: { route: "/settings/:section?" } }, },});
const routeMap = createRouteMap(machine);RouteMap / RouteMapping
The bidirectional map between the state IDs and the URL paths. It comes from @xmachines/play-router.
import { RouteMap } from "@xmachines/play-sveltekit-router";
const routeMap = new RouteMap([ { stateId: "#app.home", path: "/home" }, { stateId: "#app.dashboard", path: "/dashboard" }, { stateId: "#app.settings", path: "/settings/:section?" },]);API Summary
Exported symbols
| Export | Kind | Description |
|---|---|---|
connectRouter | function | High-level factory — connects the SvelteKit router to an actor, returns a cleanup function |
SvelteKitRouterBridge | class | The low-level bridge class. It extends RouterBridgeBase. Use it directly for full lifecycle control |
ConnectRouterOptions | type | The options object for connectRouter |
RouteMap | class | Bidirectional state ID ↔ URL path mapping (re-exported from @xmachines/play-router) |
RouteMapping | type | Single { stateId, path } entry for RouteMap construction |
RouteMapOptions | type | Options for RouteMap construction |
LocationLike | type | Minimal { pathname, search } interface for location stubs |
PlayRouteEvent | type | The event that the bridge sends to the actor on a URL change ({ type: "play.route", to, params?, query? }) |
RouterBridge | type | The interface that SvelteKitRouterBridge implements |
RoutableActor | type | Minimal actor interface from @xmachines/play-router — currentRoute, initialRoute, send(PlayRouteEvent) |
Demo
examples/demo/ holds a runnable Svelte 5 and SvelteKit demo of the adapter. Run it from the monorepo root:
pnpm installpnpm --filter @xmachines/play-sveltekit-router-demo run devThen open http://localhost:5173.
License
MIT — see LICENSE.
Classes
Interfaces
- BasePathOptions
- ConnectRouterOptions
- LocationLike
- PlayRouteEvent
- RoutableActor
- RouteMapOptions
- RouteMapping
- RouterBridge
- RouterConnection
- SvelteKitRouterBridgeOptions