03 Higher Order Behavior - christoph-fricke/xsystem GitHub Wiki

XSystem exports behavior that is useful for many different applications. Sometimes, this behavior can not be used directly and is rather used to extend behavior with common functionality. Similar to higher-order functions or higher-order components, higher-order behavior (HOB) receives a behavior and returns a new behavior.

We have already seen the usage of this concept for publishing events with the HOB withPubSub. This page includes other HOBs that are provided as well.

withSubscription

This HOB removes the boilerplate of manually subscribing and unsubscribing spawned behavior to publishers. In machines, the fromActor helper can be used to subscribe the machine for the lifecycle of an invoked service automatically. However, this is not available for pure behaviors. Instead, withSubscription can be used to subscribe for the lifecycle of the spawned behavior to the provided publisher.

import { spawnBehavior } from "xstate/lib/behaviors";
import { withSubscription, createEventBus, EventBus } from "xsystem";

type BusEvent = { type: "hello" } | { type: "world" };

function createCustomBehavior(bus: EventBus<BusEvent>) {
  // Example of using `withSubscription` to subscribe automatically to
  // published events of actors provided as factory arguments.
  return withSubscription(
    {
      initialState: "Initial State",
      transition: (state, e, ctx) => {
        // Custom behavior logic...
        return state;
      },
    },
    bus,
    ["hello"]
  );
}

const bus = spawnBehavior(createEventBus<BusEvent>());
const actor = spawnBehavior(createCustomBehavior(bus));

This feature is not fully implemented because it is currently not possible to define "onStop" behavior. Therefore, an actor is currently never unsubscribed when stopped. See the relevant issue for more information.

withHistory

This HOB provides support for undo and redo events. The functionality is based on state snapshots and not event sourcing, similar to the corresponding mobx-state-tree middleware.

Sending an undo or redo event without previous/future snapshots keeps the current state. Otherwise, undo transitions to the previous state and redo transitions to the next future state.

When an event is send while the behavior is not on top of the history stack, the future state is discarded and the sent event is put on top of the reduced state.

import { Behavior } from "xstate";
import { spawnBehavior } from "xstate/lib/behaviors";
import { withHistory, WithHistory, redo, undo, is } from "xsystem";

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

type CounterEvent = Increment | Decrement;

function createCounter(): WithHistory<Behavior<CounterEvent, number>> {
  return withHistory({
    initialState: 0,
    transition: (state, event) => {
      if (is<Increment>("increment", event)) {
        return state + (event.by ?? 1);
      }

      if (is<Decrement>("decrement", event)) {
        return state - (event.by ?? 1);
      }

      return state;
    },
  });
}

const counter = spawnBehavior(createCounter()); // => 0

counter.send({ type: "increment" }); // => 1
counter.send({ type: "increment" }); // => 2
counter.send({ type: "increment" }); // => 3

counter.send(undo()); // => 2
counter.send(undo()); // => 1
counter.send(undo()); // => 0

counter.send(redo()); // => 1
counter.send(redo()); // => 2

counter.send({ type: "decrement" }); // => 1
counter.send(redo()); // => 1 (previous future has been discarded after the decrement)
counter.send(undo()); // => 2
⚠️ **GitHub.com Fallback** ⚠️