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.nodeof every published package is now>=24.0.0. Node 24 is the Active LTS line. Node 22 is in Maintenance LTS.- The
xstatepeer 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-xstatereads 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 capabilitiesconst createPlayer = definePlayer({ machine, actor: compose(PlayerActor, withRouting, withView),});
// Routing alone: `currentView` is a compile error on this actorconst 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, andMachineGraph.getNavigableRoutes,getRoutableRoutes,routeExists, andgetRouteMappings.getTransitionReachableRoutesandisRouteReachable.
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
AbstractActoris removed. A class implements thePlayActorinterface of@xmachines/play-actor, and it extends the actor class of its own engine directly.Routablemoves from@xmachines/play-actorto@xmachines/play-router.- The router adapters no longer re-export
PlayActor. UseRoutableActorof@xmachines/play-router. BaseActorProviderPropstakes a second type parameter,TActor, with the defaultViewActor.
Peer dependencies
@xmachines/playand@xmachines/play-signalsare now peers of sixteen packages. Install both beside the packages that you use. Two copies of@xmachines/play-signalsmake two dependency graphs, and a watcher of one copy never sees a computed value of the other.xstateis 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:
PlayErrorandNonNullableErrorcome from@xmachines/play/errors.InvalidRoutePatternError,InvalidBasePathErrorandMissingBasePathParamErrorof@xmachines/play-urlcome from@xmachines/play-url/errors.guardContextWritesof@xmachines/play-viewthrowsReadOnlyContextErrorof@xmachines/play-view/errors, with the codePLAY_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.RouteMapthrowsInvalidRoutePatternErrorat 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.subscribeof@xmachines/play-dom-routerreturns aCleanup, andBrowserHistorycarries the dispose key.DisposableBrowserHistoryis removed.VanillaRoutercarries the dispose key, and it takes no type parameter.DisposableVanillaRouteris removed.connectRouterasks only for{ history }.MountFnof@xmachines/play-domreturns aCleanup.DisposablePlayUIis 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-xstateare removed:composeGuards,composeGuardsOr,negateGuard,hasContext,eventMatches, andcontextFieldMatches. Useand(),or()andnot()of XState. The XState combinators check each guard name against the guards ofsetup(). PlayerActor.dispose()is removed. Usestop(), or ausingdeclaration.dispose()of the Vue, Solid and TanStack Solid router bridges is removed. Usedisconnect().- The aliases are removed:
VueRouteMapisRouteMap,BaseRouteMappingisRouteMapping,PlayRendererPropsisActorProviderProps, andgetPlayViewContextof@xmachines/play-vueisusePlayView. MissingQueryContextError,VueRouterCorrectionError,RouteNavigateEvent, andVisibilityProviderPropsare removed. No code used them.
New features and fixes
@xmachines/play-react-routersupports React Router 8, and@xmachines/play-solid-routersupports@solidjs/router1.- 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
- Move to Node 24 and to
xstate^5.33.0. - Compose
withRoutingandwithViewinto each actor that needs them. - Change the import paths:
@xmachines/play-router/xstate,@xmachines/play-xstate/routing,@xmachines/play-view, and each/errorssubpath. - Install
@xmachines/playand@xmachines/play-signalsas direct dependencies of your application. - Escape
+,?,:,(and{in literal route paths, and remove the URLPattern polyfill import. - 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.