Skip to content

XMachines 4.0.0: routing and the view as capabilities

XMachines 4.0.0 changes the shape of an actor. An actor no longer carries routing and the view by default: it composes the capabilities that it needs. The packages split along the same line, so an application that uses no view installs no view code.

New floors: Node 24 and XState 5.33

  • engines.node of every published package is now >=24.0.0. Node 24 is the Active LTS line. Node 22 is in Maintenance LTS.
  • The xstate peer range is now ^5.33.0. XState 5.33 changes the report of a failed initial transition and of an error snapshot, and @xmachines/play-xstate reads both.

Compose the capabilities of an actor

PlayerActor no longer implements Routable and Viewable. Compose the capabilities that you need with compose:

import { definePlayer, PlayerActor, compose } from "@xmachines/play-xstate";
import { withRouting } from "@xmachines/play-xstate/routing";
import { withView } from "@xmachines/play-xstate/view";
// Both capabilities
const createPlayer = definePlayer({
machine,
actor: compose(PlayerActor, withRouting, withView),
});
// Routing alone: `currentView` is a compile error on this actor
const createRouted = definePlayer({ machine, actor: compose(PlayerActor, withRouting) });
// Neither: `state` and `send`
const createBare = definePlayer({ machine });

Compose withView after withRouting. The route then holds its new value before the view derives from the same transition.

The routing helpers of @xmachines/play-xstate move to the /routing subpath: deriveRoute, isAbsoluteRoute, buildRouteUrl, formatPlayRouteTransitions, and the route types. @xmachines/play-router and @xmachines/play-view are now OPTIONAL peers of @xmachines/play-xstate. Install each one only if you import its subpath.

The view gets its own package

@xmachines/play-view is new. It holds the view half that every renderer shares: Viewable, PlaySpec, typedSpec, BaseActorProviderProps, the context projection, and the view store lifecycle. These names move from @xmachines/play-actor. Change the import path, and add the package to your manifest.

The router splits at the machine boundary

@xmachines/play-router no longer reads a machine from its root entry. The functions that read an XState machine move to the new subpath @xmachines/play-router/xstate:

  • extractMachineRoutes, createRouteMap, machineToGraph, and MachineGraph.
  • getNavigableRoutes, getRoutableRoutes, routeExists, and getRouteMappings.
  • getTransitionReachableRoutes and isRouteReachable.

The eight router adapters no longer re-export createRouteMap. Import it from @xmachines/play-router/xstate. xstate is now an optional peer of @xmachines/play-router, and you need it only for that subpath.

The actor contracts

  • AbstractActor is removed. A class implements the PlayActor interface of @xmachines/play-actor, and it extends the actor class of its own engine directly.
  • Routable moves from @xmachines/play-actor to @xmachines/play-router.
  • The router adapters no longer re-export PlayActor. Use RoutableActor of @xmachines/play-router.
  • BaseActorProviderProps takes a second type parameter, TActor, with the default ViewActor.

Peer dependencies

  • @xmachines/play and @xmachines/play-signals are now peers of sixteen packages. Install both beside the packages that you use. Two copies of @xmachines/play-signals make two dependency graphs, and a watcher of one copy never sees a computed value of the other.
  • xstate is no longer a peer of @xmachines/play-view, of the five renderers, of the eight router adapters, or of @xmachines/play-actor.

Every error class on an /errors subpath

Each package now exports its error classes from its /errors subpath, and from there only:

  • PlayError and NonNullableError come from @xmachines/play/errors.
  • InvalidRoutePatternError, InvalidBasePathError and MissingBasePathParamError of @xmachines/play-url come from @xmachines/play-url/errors.
  • guardContextWrites of @xmachines/play-view throws ReadOnlyContextError of @xmachines/play-view/errors, with the code PLAY_VIEW_READ_ONLY_CONTEXT.

Branch on the class or on the code of an error, and never on its message.

Route metadata and the URLPattern grammar

meta.route takes an object form with a placement, a reenter flag, and data. formatPlayRouteTransitions now generates each play.route transition with reenter: false. An ancestor that stays active no longer runs its exit and entry actions. A machine that needs the old behavior declares reenter: true in its meta.route.

PlayRouteEvent no longer declares an index signature. Move a field that the interface does not name into data.

@xmachines/play-router now reads the whole URLPattern grammar, and it carries urlpattern-polyfill as a dependency. Remove the import "urlpattern-polyfill" line from your entry point. URLPatternUnavailableError is removed, because nothing can throw it.

Caution: a raw +, ?, :, ( or { in a route path now makes a pattern. In 3.0.0 the route /tags/c++ was a literal path. Now two kinds of failure are possible:

  • URLPattern refuses +, ?, and a : that no name follows. RouteMap throws InvalidRoutePatternError at startup, and the error names the pattern.
  • URLPattern accepts ( and { as grammar. The route /docs/rfc(2119) then matches /docs/rfc2119, and nothing reports a fault.

Escape the character to keep a literal route: /tags/c\+\+, /docs/rfc\(2119\), /i18n/\{en\}. A param value needs no escape.

The release protocol on every interface

3.0.0 kept three “disposable” shims beside the interfaces that a consumer implements. 4.0.0 folds them in:

  • BrowserHistory.subscribe of @xmachines/play-dom-router returns a Cleanup, and BrowserHistory carries the dispose key. DisposableBrowserHistory is removed.
  • VanillaRouter carries the dispose key, and it takes no type parameter. DisposableVanillaRouter is removed. connectRouter asks only for { history }.
  • MountFn of @xmachines/play-dom returns a Cleanup. DisposablePlayUI is removed.

A caller of these values needs no change, because a Cleanup is a () => void. A test double that implements one of the interfaces builds its release with asCleanup. @xmachines/play-dom and @xmachines/play-dom-router re-export asCleanup.

Deprecated names are removed

  • The guard utilities of @xmachines/play-xstate are removed: composeGuards, composeGuardsOr, negateGuard, hasContext, eventMatches, and contextFieldMatches. Use and(), or() and not() of XState. The XState combinators check each guard name against the guards of setup().
  • PlayerActor.dispose() is removed. Use stop(), or a using declaration.
  • dispose() of the Vue, Solid and TanStack Solid router bridges is removed. Use disconnect().
  • The aliases are removed: VueRouteMap is RouteMap, BaseRouteMapping is RouteMapping, PlayRendererProps is ActorProviderProps, and getPlayViewContext of @xmachines/play-vue is usePlayView.
  • MissingQueryContextError, VueRouterCorrectionError, RouteNavigateEvent, and VisibilityProviderProps are removed. No code used them.

New features and fixes

  • @xmachines/play-react-router supports React Router 8, and @xmachines/play-solid-router supports @solidjs/router 1.
  • The router compares the query of an actor route with the query of the router, and it refuses a restore that has nothing to put in the address bar.
  • The router pushes nothing for a mapped route that describes a set of paths.
  • The comparison that every view emission runs allocates no memory.

Upgrade

  1. Move to Node 24 and to xstate ^5.33.0.
  2. Compose withRouting and withView into each actor that needs them.
  3. Change the import paths: @xmachines/play-router/xstate, @xmachines/play-xstate/routing, @xmachines/play-view, and each /errors subpath.
  4. Install @xmachines/play and @xmachines/play-signals as direct dependencies of your application.
  5. Escape +, ?, :, ( and { in literal route paths, and remove the URLPattern polyfill import.
  6. Replace the removed guard utilities and aliases.

The routing guide of 4.0 describes the new route metadata. The next release, 5.0.0, replaces the signals with atoms.

The changelog of 4.0.0 lists every change of the release.