04 Utilities - christoph-fricke/xsystem GitHub Wiki

XSystem exports a few utilities that have proofed themself to be useful while developing the package.

createEvent

Creates an EventCreator for the given event type, which is an factory for constructing new events of that type. An additional type property is attached to the returned EventCreator function which equals the provided type and can be used to avoid "magic strings".

A optional prepare callback as the second argument to createEvent is used to attach an additional payload to events that are constructed with the event creator. The arguments received by the callback will be required as arguments for the event creator.

Furthermore, a match type predicate is attached to the returned EventCreator, which narrows given events to events with the same type.

import { createEvent } from "xsystem";

// constructing event creators.
const noData = createEvent("test.no_data");
const withData = createEvent("test.data", (id: string) => ({
  id,
  createdAt: new Date().toISOString(),
}));

//using the event creators
console.log(withData("123")); // => { type: "test.data", id: "123", createdAt: "2022-01-07T14:02:25.893Z" }
console.log(withData.type); // => "test.data"
someActor.send(noData()); // => sends { type: "test.no_data" } to someActor
noData.match({ type: "other" }); // => false
noData.match({ type: "test.no_data" }); // => true returned as matching type predicate

It is possible to use the type property of event creators in machine definitions. This has the advantage that it is easier to refactor the event type. However, it prevents XState from correctly visualizing machines in its VS Code extension because definitions are parsed statically. It works fine in the Stately Visualizer.

Connecting Actors with UIs (createSendCall)

When we connect our user interface to actors, we have to handle UI events and convert and send them to corresponding actor.

Inside components, code such as the following is not uncommon:

import { useInterpret } from "@xstate/react";
import { createEvent } from "xsystem";
import { createActor } from "./actor";

// Would properly be defined somewhere else. Displayed here for clarity.
const event = createEvent("test.event", (data: string) => ({ data }));

function Component() {
  const actor = useInterpret(createActor);

  const handleData = (data: string) => actor.send(event(data));

  return <OtherComponent onData={handleData}>Button</OtherComponent>;
}

Writing these event-handlers (handleData) over and over again becomes tedious. Especially, when they just proxy parameters the event-creator.

Therefore, each event-creator contains a createSendCall function to create such event-handlers for a given ActorRef. This should help with reducing boilerplate and should avoid a lot of work when many event-handlers have to be created. The above code can be simplified to the following:

import { useInterpret } from "@xstate/react";
import { createEvent } from "xsystem";
import { createActor } from "./actor";

const event = createEvent("test.event", (data: string) => ({ data }));

function Component() {
  const actor = useInterpret(createActor);

  const handleData = event.createSendCall(actor);

  return <OtherComponent onData={handleData}>Button</OtherComponent>;
}

EventFrom

Helper type that extracts the event shape from an EventCreator. Extremely useful to create types for events that a machine or behavior can receive. If the provided generic does not extend EventCreator, it falls back to the original EventFrom implementation in XState.

import { createEvent, EventFrom } from "xsystem";

const noData = createEvent("test.no_data");
const withData = createEvent("test.data", (id: string) => ({
  id,
  createdAt: new Date().toISOString(),
}));

type NoDataEvent = EventFrom<typeof noData>;
// { type: "test.no_data" }

type WithDataEvent = EventFrom<typeof withData>;
// { type: "test.data", id: string, createdAt: string }

is

A type predicate that narrows a given event to a specific event based on the event type. It is most useful when defining a transition function for Behavior. If an EventCreator exists for the event in question, creator.match(event) should be preferred because it is more ergonomic.

import { is } from "xsystem";

type Increment = { type: "increment"; by: number };
type Decrement = { type: "decrement"; by: number };

const event = {} as Increment | Decrement;

if (is<Increment>("increment", event)) {
  // event is narrowed to an `Increment` event.
}

if (is<Decrement>("decrement", event)) {
  // event is narrowed to an `Decrement` event.
}
⚠️ **GitHub.com Fallback** ⚠️