Multi-Router Integration - Renderer Prop Pattern
Learn how to integrate Play Architecture with three router modes using the unified renderer prop pattern introduced in
Overview
introduces a unified multi-router architecture supporting three integration modes with consistent API:
- TanStack Router - React + TanStack Router with full feature set
- Vanilla Router - JSX frameworks (Preact, Solid, Vue) with framework-agnostic router
- Pure Browser - Manual integration for jQuery, Alpine, vanilla JS
All modes use the renderer prop pattern where providers receive an actor and router, then pass a renderer function (not children).
Key Concepts
1. Renderer Prop Pattern
All providers use a renderer prop that receives the actor and returns a ReactNode:
<PlayTanStackRouterProvider actor={actor} router={router} renderer={(actor) => <PlayRenderer actor={actor} components={components} />}/>Why renderer prop instead of children?
- Explicit dependency injection (actor passed to renderer function)
- Type-safe integration (renderer function signature enforced)
- Consistent pattern across all three modes
2. RouteMap as Explicit Prop
All providers require routeMap as an explicit prop. Routers fundamentally don’t know about Play state IDs — the map bridges the gap:
const routeTree = createPlayRouter({ machine });const router = createRouter({ routeTree, history: createMemoryHistory() });
<PlayTanStackRouterProvider actor={actor} router={router} routeMap={routeMap} // Explicit mapping renderer={(actor) => <PlayRenderer actor={actor} components={components} />}/>3. Provider Composition Pattern
Providers wrap external providers (like TanStack’s RouterProvider) and add Play integration layer:
// Provider wraps TanStack RouterProvider<RouterProvider router={router}> {/* Play integration layer */} {renderer(actor)}</RouterProvider>Mode 1: TanStack Router (React)
Complete integration with React and TanStack Router.
See multi-router-example.ts for runnable example.
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import { createRouter, createMemoryHistory } from '@tanstack/react-router';import { createPlayRouter, PlayTanStackRouterProvider } from '@xmachines/play-tanstack-react-router';import { PlayRenderer } from '@xmachines/play-react';import { definePlayer } from '@xmachines/play-xstate';
// 1. Create actorconst createPlayer = definePlayer({ machine, catalog });const actor = createPlayer();
// 2. Create router from machineconst routeTree = createPlayRouter({ machine });const router = createRouter({ routeTree, history: createMemoryHistory()});
// 3. Define React componentsconst components = { HomeView: ({ send }) => ( <div> <h1>Home</h1> <button onClick={() => send({ type: 'play.route', to: '/about' })}> Go to About </button> </div> ), AboutView: ({ send }) => ( <div> <h1>About</h1> <button onClick={() => send({ type: 'play.route', to: '/' })}> Go to Home </button> </div> )};
// 4. Render with provider + renderer propfunction App() { return ( <PlayTanStackRouterProvider actor={actor} router={router} renderer={(actor) => <PlayRenderer actor={actor} components={components} />} /> );}
const root = createRoot(document.getElementById('app')!);root.render(<StrictMode><App /></StrictMode>);Benefits
- Full React ecosystem support
- Type-safe routing with TanStack Router
- Browser history integration automatic
- Signal reactivity via
PlayRenderer
Mode 2: Vanilla Router (Preact, Solid, Vue)
Framework-agnostic router for JSX-based frameworks.
import { createPlayRouter, connectRouter } from '@xmachines/play-router';import { definePlayer } from '@xmachines/play-xstate';
// 1. Create actorconst createPlayer = definePlayer({ machine, catalog });const actor = createPlayer();
// 2. Create framework-agnostic routerconst router = createPlayRouter({ machine });
// 3. Connect router with manual integrationconst disconnect = connectRouter(actor, { onNavigate: (path) => { // Update your framework's router myFrameworkRouter.push(path); }});
// 4. Framework-specific rendering// Preact:import { h } from 'preact';const view = actor.currentView.get();render(h(components[view.component], viewProps), container);
// Solid:import { Dynamic } from 'solid-js/web';<Dynamic component={components[view.component]} {...viewProps} />
// Vue 3:import { h, resolveComponent } from 'vue';h(resolveComponent(view.component), viewProps)Benefits
- Works with any JSX framework
- Manual control over routing integration
- No React dependency
- Same actor/signal API
Mode 3: Pure Browser (jQuery, Alpine, Vanilla JS)
Manual browser integration with no framework.
import { createPlayRouter, connectRouter } from "@xmachines/play-router";
// 1. Create actorconst actor = createPlayer();
// 2. Connect router with browser integrationconst disconnect = connectRouter(actor, { onNavigate: (path) => { // Update browser history window.history.pushState({}, "", path); renderView(); },});
// 3. Manual renderingfunction renderView() { const view = actor.currentView.get();
// Vanilla JS rendering if (view.component === "HomeView") { document.getElementById("app").innerHTML = ` <h1>Home</h1> <button onclick="navigateToAbout()">Go to About</button> `; } else if (view.component === "AboutView") { document.getElementById("app").innerHTML = ` <h1>About</h1> <button onclick="navigateToHome()">Go to Home</button> `; }}
// 4. Listen to browser back/forwardwindow.addEventListener("popstate", () => { const path = window.location.pathname; actor.send({ type: "play.route", to: path });});
// 5. Manual event handlersfunction navigateToAbout() { actor.send({ type: "play.route", to: "/about" });}
function navigateToHome() { actor.send({ type: "play.route", to: "/" });}
// Startactor.start();renderView();Benefits
- No framework dependency at all
- Maximum control and flexibility
- Works with jQuery, Alpine.js, HTMX, etc.
- Signals available via
actor.currentView.get()
Provider API Parallelism
All three modes follow the same pattern:
| Concept | TanStack Mode | Vanilla Mode | Pure Browser Mode |
|---|---|---|---|
| Actor | createPlayer() | createPlayer() | createPlayer() |
| Router | createRouter() (TanStack) | createPlayRouter() | connectRouter() |
| Integration | <PlayTanStackRouterProvider> | Manual via onNavigate callback | Manual + window.popstate |
| Rendering | <PlayRenderer> via renderer prop | Framework-specific (h, Dynamic, etc) | innerHTML or DOM API |
| Events | Component onClick → send() | Same | Same |
| Signals | Observed by useSignalEffect | Manual actor.currentView.get() | Same |
The API parallelism ensures consistent patterns regardless of mode.
Architectural Invariants
All three modes preserve the 5 architectural invariants:
- Actor Authority (INV-01): Guards validate all navigation across all modes
- Strict Separation (INV-02): Business logic (machine) has zero framework imports
- Signal-Only Reactivity (INV-05): TC39 Signals work identically in all modes
- Passive Infrastructure (INV-04): Router observes actor, never decides
- State-Driven Reset (INV-03): Browser back/forward sends events to actor
Next Steps
- Routing Patterns - Learn play.route events with parameters
- Integration Example - Complete authentication flow
- React + TanStack Router Demo - Production example using TanStack mode
- Vue Router Demo - Vue Composition API integration
- SolidJS Router Demo - Solid signals integration
Learn More
- play-tanstack-router README - TanStack integration details
- play-react README - React renderer documentation
- play-router README - Framework-agnostic routing
- RFC Play v1 - Complete specification