npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xmachines/play-tanstack-react-router

v2.1.0

Published

TanStack React Router adapter for XMachines Play - synchronizes browser URL with actor state through passive infrastructure

Readme

@xmachines/play-tanstack-react-router

TanStack Router (React) adapter for XMachines Play. It keeps the browser URL and the actor state in step through passive infrastructure.

License: MIT Version

Installation

pnpm add @xmachines/play-tanstack-react-router

Peer dependencies. Install them separately:

pnpm add @tanstack/react-router react react-dom xstate

The adapter requires:

  • @tanstack/react-router ^1.168.8
  • react ^18.0.0 or ^19.0.0
  • react-dom ^18.0.0 or ^19.0.0
  • xstate ^5.31.0

Usage

PlayRouterProvider — React component (recommended)

PlayRouterProvider is the primary integration point. It creates a TanStackReactRouterBridge on mount. It keeps the bridge connected for the life of the component. It disconnects the bridge on unmount.

import { useMemo, useEffect, useState } from "react";
import { createMachine } from "xstate";
import { createRouter, createRootRoute } from "@tanstack/react-router";
import { PlayRouterProvider, createRouteMap } from "@xmachines/play-tanstack-react-router";
import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
import { useSignalEffect } from "@xmachines/play-react";

// Any machine whose states declare `meta.route` (plus an explicit `id`) is routable.
// formatPlayRouteTransitions() generates the root-level `play.route` handlers that
// let the bridge drive the machine from URL changes.
const machine = createMachine(
	formatPlayRouteTransitions({
		id: "app",
		initial: "home",
		states: {
			home: { id: "home", meta: { route: "/" } },
			about: { id: "about", meta: { route: "/about" } },
		},
	}),
);

const createPlayer = definePlayer({ machine });
type AppActor = ReturnType<typeof createPlayer>;

// Minimal shell: mirrors the actor's route signal into React state. A full app
// renders <PlayUIProvider> + <PlayRenderer> from @xmachines/play-react here
// instead — see examples/demo for the complete Shell.
function Shell({ actor }: { actor: AppActor }) {
	const [route, setRoute] = useState(actor.currentRoute.get());
	useSignalEffect(() => setRoute(actor.currentRoute.get()), [actor]);
	return <p>Current route: {route}</p>;
}

function createAppRuntime() {
	const actor = createPlayer();
	actor.start();

	const routeMap = createRouteMap(machine);
	const rootRoute = createRootRoute();
	const router = createRouter({ routeTree: rootRoute });

	return { actor, routeMap, router };
}

export function App() {
	// All three props must be stable references — memoize to avoid reconnecting on every render
	const { actor, routeMap, router } = useMemo(createAppRuntime, []);

	useEffect(() => () => actor.stop(), [actor]);

	return (
		<PlayRouterProvider
			actor={actor}
			router={router}
			routeMap={routeMap}
			renderer={(currentActor) => <Shell actor={currentActor} />}
		/>
	);
}

Stable references: actor, router, and routeMap must stay stable across the renders. If one prop gets a new identity, the bridge disconnects, then it connects again. Use useMemo to create each prop one time.

TanStackReactRouterBridge — the bridge class

Use the bridge directly when you do not need the React wrapper, or when you integrate it with a custom lifecycle:

import { createRouter, createRootRoute } from "@tanstack/react-router";
import { definePlayer } from "@xmachines/play-xstate";
import { TanStackReactRouterBridge, createRouteMap } from "@xmachines/play-tanstack-react-router";
import { machine } from "./machine.js"; // the routable machine from the example above

const router = createRouter({ routeTree: createRootRoute() });
const actor = definePlayer({ machine })();
actor.start();
const routeMap = createRouteMap(machine);

const bridge = new TanStackReactRouterBridge(router, actor, routeMap);
bridge.connect();

// Cleanup when done
bridge.disconnect();

API Summary

TanStackReactRouterBridge

This class extends RouterBridgeBase from @xmachines/play-router. It keeps the actor state signals and the TanStack Router history in step, in both directions.

class TanStackReactRouterBridge extends RouterBridgeBase {
	constructor(router: TanStackRouterLike, actor: RoutableActor, routeMap: RouteMap);

	connect(): void; // Start sync; subscribe to router.history and actor signals
	disconnect(): void; // Stop sync; unsubscribe all listeners
}

The bridge subscribes to router.history, not to router.subscribe("onBeforeLoad"). Therefore the bridge also receives a browser BACK or FORWARD navigation (a popstate event) when no <RouterProvider> is mounted.

PlayRouterProvider

This React component wraps TanStackReactRouterBridge in a useEffect lifecycle.

interface PlayRouterProviderProps<TActor> {
	actor: TActor; // Must be stable
	router: TanStackRouterInstance; // Must be stable
	routeMap: RouteMap; // Must be stable
	renderer: (actor: TActor, router: TanStackRouterInstance) => ReactNode;
}

TanStackRouterLike

The structural type of the router instance. It accepts every object that has the necessary navigate and history shape. A test can therefore use a stub in place of a complete TanStack Router:

type TanStackRouterLike = {
	navigate(args: { to: string }): void;
	load?(): void | Promise<void>;
	history: {
		location: { pathname: string; search?: string };
		subscribe(
			handler: (event: { location: { pathname: string; search?: string } }) => void,
		): () => void;
	};
};

RouteNavigateEvent

The event that the bridge sends to the actor when the browser navigates:

interface RouteNavigateEvent {
	readonly type: "route.navigate";
	readonly path: string; // e.g. "/dashboard" or "/posts/123"
}

Re-exported from @xmachines/play-router

// Route map construction
RouteMap
createRouteMap(machine, options?): RouteMap
createRouteMapFromTree(routeTree): RouteMap
extractMachineRoutes(machine): RouteTree

// Types
type RouteMapOptions
type RouteMapping
type RouterBridge
type PlayRouteEvent

How It Works

The bridge implements the Passive Infrastructure invariant from the XMachines RFC:

  1. Actor → Router: when the actor.currentRoute signal changes, the bridge calls router.navigate({ to: path }). The URL then shows the new actor state.
  2. Router → Actor: when router.history.subscribe fires, the bridge sends a play.route event to the actor. A link click, a BACK or FORWARD button, and a call to history.pushState each cause this. The guards of the actor decide if the navigation is valid. The router never enforces the business logic.
  3. Circular update prevention: the lastSyncedPath guard stops a return update. An actor-to-router navigation therefore does not cause an unnecessary router-to-actor send.
  4. Deep-link and restore: on connect(), the bridge reads router.history.location.pathname. That value shows window.location at once, before router.load() runs. The bridge then makes a decision: it sets the actor state from the URL (a deep link), or it writes the restored route of the actor to the URL (a snapshot restore).

Testing

Run tests for this package in isolation:

pnpm --filter @xmachines/play-tanstack-react-router test

From the monorepo root:

pnpm test

Tests cover RouterBridge protocol compliance, actor ↔ router bidirectional sync, circular update prevention, deep-link and snapshot-restore scenarios, and PlayRouterProvider lifecycle (mount/unmount/reconnect).

Demo

examples/demo/ holds a runnable demo of the React and TanStack Router integration. Run it from the repository root:

pnpm install
pnpm --filter @xmachines/play-tanstack-react-router-demo run dev

Then open http://localhost:3011.

The demo shows actor-authoritative routing with a shared auth machine. TanStack Router updates the URL. PlayRouterProvider converts the update into a play.route event. The guards of the actor then permit the access, or they refuse it.

License

MIT — see LICENSE.