Skip to content

@xmachines/play-solid

Documentation / @xmachines/play-solid

SolidJS renderer consuming signals and UI schema with provider pattern

Signal-driven SolidJS rendering layer observing actor state with zero SolidJS state for business logic.

Overview

@xmachines/play-solid provides PlayRenderer for building SolidJS UIs that passively observe actor signals. This package enables framework-swappable architecture where SolidJS is just a rendering target subscribing to signal changes — business logic lives entirely in the actor.

Per RFC Play v1, this package implements:

  • Signal-Only Reactivity (INV-05): No createSignal/createStore for business logic, TC39 signals only
  • Passive Infrastructure (INV-04): Components observe signals, send events to actor

Key Principle: SolidJS state is never used for business logic. Signals are the source of truth.

Renderer receives actor via props (provider pattern), not children.

Installation

Terminal window
npm install solid-js@^1.8.0
npm install @xmachines/play-solid

Current Exports

  • PlayRenderer
  • PlayRendererProps (type)

Peer dependencies:

  • solid-js ^1.8.0 - SolidJS runtime

Quick Start

import { render } from "solid-js/web";
import { definePlayer } from "@xmachines/play-xstate";
import { defineCatalog } from "@xmachines/play-catalog";
import { PlayRenderer } from "@xmachines/play-solid";
import { z } from "zod";
// 1. Define catalog (business logic layer)
const catalog = defineCatalog({
LoginForm: z.object({ error: z.string().optional() }),
Dashboard: z.object({
userId: z.string(),
username: z.string(),
}),
});
// 2. Create SolidJS components (view layer)
const components = {
LoginForm: (props) => (
<form
onSubmit={(e) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
props.send({
type: "auth.login",
username: data.get("username"),
});
}}
>
{props.error && <p style={{ color: "red" }}>{props.error}</p>}
<input name="username" required placeholder="Username" />
<button type="submit">Log In</button>
</form>
),
Dashboard: (props) => (
<div>
<h1>Welcome, {props.username}!</h1>
<p>User ID: {props.userId}</p>
<button onClick={() => props.send({ type: "auth.logout" })}>Log Out</button>
</div>
),
};
// 3. Create player actor (business logic runtime)
const createPlayer = definePlayer({ machine: authMachine, catalog });
const actor = createPlayer();
actor.start();
// 4. Render UI (actor via props)
render(
() => <PlayRenderer actor={actor} components={components} />,
document.getElementById("app")!,
);

API Reference

PlayRenderer

Main renderer component subscribing to actor signals and dynamically rendering catalog components:

interface PlayRendererProps {
actor: AbstractActor<any>;
components: Record<string, Component<any>>;
fallback?: JSX.Element;
}

Props:

  • actor - Actor instance with currentView signal
  • components - Map of component names to SolidJS components
  • fallback - Component shown when currentView is null

Behavior:

  1. Subscribes to actor.currentView signal using a Signal.subtle.Watcher inside the component
  2. Looks up component from components map using view.component string
  3. Renders component with props from view.props + send function using Solid’s <Dynamic />

Example:

<PlayRenderer
actor={actor}
components={{
HomePage: (props) => <div>Home</div>,
AboutPage: (props) => <div>About</div>,
}}
fallback={<div>Loading...</div>}
/>

Examples

Component Receiving Props from Catalog

import { PlayRenderer } from "@xmachines/play-solid";
import { defineCatalog } from "@xmachines/play-catalog";
import { z } from "zod";
// Define schema in catalog
const catalog = defineCatalog({
UserProfile: z.object({
userId: z.string(),
name: z.string(),
avatar: z.string().url().optional(),
stats: z.object({
posts: z.number(),
followers: z.number(),
}),
}),
});
// Component receives type-safe props + send
const components = {
UserProfile: (props) => (
<div>
{props.avatar && <img src={props.avatar} alt={props.name} />}
<h1>{props.name}</h1>
<p>ID: {props.userId}</p>
<div>
<span>{props.stats.posts} posts</span>
<span>{props.stats.followers} followers</span>
</div>
<button
onClick={() =>
props.send({
type: "profile.edit",
userId: props.userId,
})
}
>
Edit Profile
</button>
</div>
),
};
<PlayRenderer actor={actor} components={components} />;

Provider Pattern

import { PlayTanStackRouterProvider } from "@xmachines/play-tanstack-solid-router";
import { PlayRenderer } from "@xmachines/play-solid";
import { createSignal, onCleanup } from "solid-js";
// Renderer receives actor via props (not children)
function App() {
return (
<PlayTanStackRouterProvider
actor={actor}
router={router}
routeMap={routeMap}
renderer={(currentActor, currentRouter) => {
return (
<div>
<Header actor={currentActor} />
<PlayRenderer actor={currentActor} components={components} />
<Footer />
</div>
);
}}
/>
);
}
// Header component also receives actor
function Header(props) {
const [route, setRoute] = createSignal<string | null>(null);
// Manual watcher setup for custom component reading signals
let watcher: Signal.subtle.Watcher;
// ... setup watcher to update route signal ...
return (
<header>
<nav>Current: {route()}</nav>
</header>
);
}

Architecture

This package implements Signal-Only Reactivity (INV-05) and Passive Infrastructure (INV-04):

  1. No Business Logic in SolidJS:

    • No createSignal/createStore for business state
    • No createEffect for business side effects
    • SolidJS only triggers renders, doesn’t control state
  2. Signals as Source of Truth:

    • actor.currentView.get() provides UI structure
    • actor.currentRoute.get() provides navigation state
    • Components observe signals via explicit watcher patterns
  3. Event Forwarding:

    • Components receive send function via props
    • User actions send events to actor (e.g., { type: "auth.login" })
    • Actor guards validate and process events
  4. Microtask Batching:

    • Signal.subtle.Watcher coalesces rapid signal changes
    • Prevents SolidJS thrashing from multiple signal updates
    • Single SolidJS render per microtask batch
  5. Explicit Disposal Contract:

    • Component teardown calls watcher unwatch in onCleanup
    • Do not rely on GC-only cleanup

Pattern:

  • Renderer receives actor via props (provider pattern)
  • Enables composition with navigation, headers, footers
  • Supports multiple renderers in same app

Architectural Invariants:

  • Signal-Only Reactivity (INV-05): No SolidJS state for business logic
  • Passive Infrastructure (INV-04): Components reflect, never decide

Canonical Watcher Lifecycle

If you write your own custom integration, use the same watcher flow as PlayRenderer:

  1. notify callback runs
  2. Schedule work with queueMicrotask
  3. Drain watcher.getPending()
  4. Read actor signals and update SolidJS-local signal state (setSignal())
  5. Re-arm with watch(...) or watch()

Watcher notify is one-shot. Re-arm is required for continuous observation.

Benefits

  • Framework Swappable: Business logic has zero SolidJS imports
  • Type Safety: Props validated against catalog schemas
  • Simple Testing: Test actors without SolidJS renderer
  • Performance: Microtask batching reduces unnecessary renders
  • Composability: Renderer prop enables complex layouts

License

Copyright (c) 2016 Mikael Karon. All rights reserved.

This work is licensed under the terms of the MIT license.
For a copy, see https://opensource.org/licenses/MIT.

SolidJS renderer for XMachines Play architecture

Interfaces

Variables