Skip to content

XMachines 7.0.0: one error contract for five renderers

XMachines 7.0.0 gives the five renderers one rule for a failure of a view. It moves json-render to the fork release 0.21.0-xm.6, which reports each render failure with its kind. The release also adds warnings for the ids and the state keys of a machine, and it supports SvelteKit 3. Ten changes are breaking. Most of them change a rare case, and the upgrade section lists each step.

A failure of the root is a failure of the view

json-render 0.21.0-xm.6 calls onRenderError with (error, name, info). info.kind names the failure: "element", "props", "visible", "action", "watch", or "root". info.key, info.type and info.action name the part that failed. A handler with two parameters stays valid:

import type { OnRenderError } from "@xmachines/play-react";
const onRenderError: OnRenderError = (error, name, info) => {
console.warn(`${info.kind} failed at ${info.key ?? name}`, error);
};

@xmachines/play-react, @xmachines/play-vue, @xmachines/play-solid, @xmachines/play-svelte and @xmachines/play-dom export OnRenderError, RenderErrorInfo and RenderErrorKind.

A failure of the root — its component, a prop, or its visible condition — leaves no view on the screen. json-render gives it to no error boundary of the host. With 6.1.0 and json-render 0.21.0-xm.5, a root whose prop failed left an empty screen, and onError stayed silent. In 7.0.0 each of the five providers contains such a failure as a failure of the view. It shows its fallback, it calls onError one time with the reset, and the next view renders again. onRenderError also receives the failure, with the kind "root".

A failure below the root reaches onRenderError alone, and the rest of the view stays. The views guide states the rule. A shared contract suite runs it against all five renderers.

The onError of play-react takes the reset second

The onError prop of the other four providers takes (error, reset). play-react took (error, info, reset), so one handler for two renderers read the React.ErrorInfo as the reset. The onError prop of ActorProvider and PlayUIProvider of @xmachines/play-react now takes (error, reset, info):

// 6.1.0
<PlayUIProvider onError={(error, info, reset) => report(error, info)} /* ... */ />
// 7.0.0
<PlayUIProvider onError={(error, reset, info) => report(error, info)} /* ... */ />

Warnings for the ids and the state keys of a machine

XState does not check that the ids of the states are unique. @xmachines/play-xstate keys the route, the view and its store on the id of a state. Two states with one id therefore shared one route entry, one store and one key, and nothing told you. An id that is an array index, such as "42", also lost its place in the active branch, because JavaScript lists such a key first.

play-xstate now writes one warning for each machine that repeats an id or uses an array index as an id. The state machines guide states the rule: give each state its own id.

The views of one layout share the store of the layout. Two pages that each declare the key query therefore share one value, and the second page finds the input of the first. play-xstate now reads the state nodes of each machine one time, and it writes one warning for each layout. The warning names each state key that two of its views declare, with the ids of their states. It also finds two pages that are never on the screen at the same time.

The view derivation

play-xstate now derives every view with one walk of the state tree. A parallel state tries its routed region first, so the view follows the region of the URL. Three cases of a parallel machine show a different view than in 6.1.0:

  • The routed region sits in a state with no route of its own, such as a load. The view now comes from that region, and not from an earlier region.
  • The routed region holds no view. The view now comes from the next region that resolves to one, and not from a view on the parallel state.
  • No region holds a route, and the first region holds no view. The view now comes from the first region that holds one, and not from the last region.

In each case the viewKey changes, and the view store seeds again. Put the view that must show into the routed region, or declare outlets on the parallel state. The views guide states the rule.

@xmachines/play-view changes the composition of a layout in two ways:

  • A key of the outer view that equals the prefixed key of a child element, such as "page/root", now throws ViewElementKeyCollisionError from @xmachines/play-view/errors. Before, one of the two elements went missing with no message. The actor reports the error through its onError option, and it keeps its last good view. Do not start a key of the outer view with the id of a state and a /.
  • The prefix of a child key escapes a ~ or a / of the state id as ~0 and ~1. The key "c" of the child "a/b" therefore becomes "a~1b/c".

composeViews now makes a new object on each call. play-xstate keeps one result for each tree. A custom caller that emits the composed view on each snapshot must do the same, or an emit gate that compares by reference sees a change on each snapshot.

play-dom follows the other renderers

@xmachines/play-dom changes three behaviours, so that it acts as the four framework renderers do:

  • A failed rebuild keeps the store of the view. 6.1.0 seeded a new store from spec.state, and the ephemeral state of the failed attempt went away. Emit a new viewKey to drop it.
  • A binding with confirm and no onConfirm waits for the user, and a default confirm dialog shows it. 6.1.0 skipped such a binding with a warning. Set confirmDialog: false to turn the dialog off.
  • A store write shows at its commit point, a microtask after the write, and not at once.

SvelteKit 3, and shallow: "sync"

@xmachines/play-sveltekit-router supports SvelteKit 3. SvelteKit 3 replaces the noScroll and keepFocus options of goto with reset: false. The bridge reads the version of @sveltejs/kit, and it passes the options of that major.

A new option, shallow, sets what the bridge does with a shallow navigation. A shallow navigation is goto(url, { shallow: true }), pushState, replaceState, or a back or forward step between shallow entries:

const disconnect = connectRouter({ actor, routeMap: createRouteMap(machine), shallow: "sync" });
  • shallow: "ignore" is the default. The machine keeps its route, as before.
  • shallow: "sync" makes the machine follow each URL that the browser shows. Give each shallow URL a route in the machine.

The bridge reads page of $app/state, which starts at SvelteKit 2.12.0. The peer range is now ^2.12.0 || ^3.0.0. The shallow navigation section of the bridge describes the limits of each major.

@xmachines/play-router compared the path alone when it decided between a deep link and a restore. A router at /?other=1 therefore read as “at the initial URL /”. A restored actor won, and the bridge wrote the query of the actor over the query that the user opened.

The bridge now compares the path and the query. A router at the initial path with a query of its own is a deep link, and the router wins. After a reload, a query that the host or the browser adds to the initial URL therefore drops the restored route. To keep the restore, load the app at the initial URL with no query.

json-render 0.21.0-xm.6

Each @xmachines/json-render-* peer of play-dom, play-react, play-solid, play-svelte, play-view and play-vue is now the exact version 0.21.0-xm.6, and not a range. A caret range accepts each -xm.N release, and an -xm.N release can break, as this one does. The vue peer of play-vue is now ^3.5.11, and the solid-js peer of play-solid is now ^1.9.8.

A custom provider

play-view no longer exports createFailureLatch, sameViewInputs, FailureLatch and ViewInputs. A custom provider reads createViewSession instead. The session resolves one time for each attempt, and a new snapshot starts a new attempt. The play-view reference describes it.

Upgrade

  1. Move the @xmachines/play-* packages to 7.0.0.
  2. Move each @xmachines/json-render-* package to the exact version 0.21.0-xm.6.
  3. Move vue to 3.5.11 or later, and solid-js to 1.9.8 or later.
  4. With play-react, swap the second and the third parameter of each onError handler.
  5. With play-sveltekit-router, move SvelteKit 2 to 2.12.0 or later.
  6. With play-dom, set confirmDialog: false if a binding with confirm must not show the default dialog.
  7. Read the warnings of play-xstate for repeated ids and shared state keys, and fix each one.
  8. Check each parallel machine without outlets against the three cases of the view derivation.

The previous release is 6.1.0. The changelog of 7.0.0 lists every change of the release.