Skip to content

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

License: MIT Version

Installation

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

Terminal window
pnpm add @sveltejs/kit svelte
Peer dependencyRequired 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 capability
const createPlayer = definePlayer({ machine: myMachine, actor: compose(PlayerActor, withRouting) });
const actor = createPlayer();
actor.start();
// 2. Build a bidirectional route map from the machine definition
const 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 destroyed
onDestroy(() => disconnect());

The bridge works in both directions:

  • URL → actor: the bridge converts each SvelteKit afterNavigate event into a play.route event, then sends it to the actor. With shallow: "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 currentRoute atom to the browser URL with goto().

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 above
declare const machine: AnyStateMachine;
declare const actor: RoutableActor;
const routeMap = createRouteMap(machine);
// In a Svelte component or SvelteKit layout
const disconnect = connectRouter({ actor, routeMap });
// Or pass an explicit location for SSR or test environments
const disconnectSsr = connectRouter({
actor,
routeMap,
location: { pathname: "/dashboard", search: "?tab=stats" },
});
// Cleanup
onDestroy(() => disconnect());

ConnectRouterOptions:

PropertyTypeRequiredDescription
actorRoutableActorYesThe XMachines actor that the bridge keeps in step with the router
routeMapRouteMapYesThe bidirectional map between the state IDs and the URL paths
locationLocationLike | nullNoThe 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"NoWhat 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 watches afterNavigate.
  • shallow: "sync": the machine follows each URL that the browser shows, the shallow URLs too, back and forward included. The bridge watches page of $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 above
declare 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 above
declare const machine: AnyStateMachine;
declare const actor: RoutableActor;
const routeMap = createRouteMap(machine);
const bridge = new SvelteKitRouterBridge(actor, routeMap, location);
bridge.connect();
// On teardown
bridge.disconnect();

SvelteKit integration details:

  • navigateRouter → goto(path, { reset: false }) on SvelteKit 3, and goto(path, { noScroll: true, keepFocus: true }) on SvelteKit 2. The bridge reads VERSION of @sveltejs/kit to choose the options. Both keep the scroll position and the focused element. On the server, the bridge calls no goto: SvelteKit has none there
  • watchRouterChanges → registers an afterNavigate callback through an indirection cell, which prevents a direct capture of this. The callback ignores a shallow navigation (goto(url, { shallow: true })), because the URL changes and no page loads
  • unwatchRouterChanges → sets the indirection cell to null, which releases the bridge reference before the component unmounts (SvelteKit has no API to cancel afterNavigate)

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

ExportKindDescription
connectRouterfunctionHigh-level factory — connects the SvelteKit router to an actor, returns a cleanup function
SvelteKitRouterBridgeclassThe low-level bridge class. It extends RouterBridgeBase. Use it directly for full lifecycle control
ConnectRouterOptionstypeThe options object for connectRouter
RouteMapclassBidirectional state ID ↔ URL path mapping (re-exported from @xmachines/play-router)
RouteMappingtypeSingle { stateId, path } entry for RouteMap construction
RouteMapOptionstypeOptions for RouteMap construction
LocationLiketypeMinimal { pathname, search } interface for location stubs
PlayRouteEventtypeThe event that the bridge sends to the actor on a URL change ({ type: "play.route", to, params?, query? })
RouterBridgetypeThe interface that SvelteKitRouterBridge implements
RoutableActortypeMinimal 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:

Terminal window
pnpm install
pnpm --filter @xmachines/play-sveltekit-router-demo run dev

Then open http://localhost:5173.

License

MIT — see LICENSE.

Classes

Interfaces

Type Aliases

Functions