Skip to content
Version: XState v4

Node

When running machines in Node, a few common patterns emerge.

Long-running processes

The first pattern is running XState to coordinate a long-running process, for instance, a file watcher in a CLI (Command Line Interface):

import { interpret, createMachine } from "xstate";
import { watch } from "chokidar";

const machine = createMachine({
initial: "waitingForChokidarToBeReady",
states: {
waitingForChokidarToBeReady: {
on: {
CHOKIDAR_READY: {
target: "chokidarReady",
},
},
},
chokidarReady: {
entry: () => {
console.log("Chokidar Ready!");
},
},
},
});

const main = () => {
const actor = interpret(machine).start();

watch("./**").on("ready", () => {
actor.send("CHOKIDAR_READY");
});
};

main();

In the script above, we run our main actor by calling interpret().start() on the machine. We use chokidar, a file watcher, to watch over a set of files and then report that the watcher is ready to the actor. The actor then logs a message to the console.

XState is extremely powerful at managing long-running processes like this, where multiple event sources need to be coordinated. You could even extend the example above to handle a process, which Gatsby does for its build processing.

Async functions

Lots of backend code relies on short-running processes, such as backend functions, especially in serverless contexts where code needs to boot up and shut down as fast as possible.

Much of this type of code relies on async functions:

const myFunc = async () => {};

The best pattern to use for async functions is waitFor, which allows you to await a state machine being in a particular state.

import { interpret, createMachine } from "xstate";
import { waitFor } from "xstate/lib/waitFor";

const machine = createMachine({
initial: "pending",
states: {
pending: {
after: {
3000: {
target: "done",
},
},
},
done: {},
},
});

const myFunc = async () => {
const actor = interpret(machine).start();

const doneState = await waitFor(actor, (state) =>
state.matches("done"),
);

console.log(doneState.value); // 'done'
};

In the example above, the machine waits for three seconds before moving on to its done state, at which point the await will resolve, and the program will move on.

By default, waitFor will time out after 10 seconds if the desired state is not reached. You can customize this timeout by passing timeout in the options:

import { interpret, createMachine } from "xstate";
import { waitFor } from "xstate/lib/waitFor";

const machine = createMachine({
initial: "pending",
states: {
pending: {
after: {
3000: {
target: "done",
},
},
},
done: {},
},
});

const myFunc = async () => {
const actor = interpret(machine).start();

const doneState = await waitFor(
actor,
(state) => state.matches("done"),
{
// 20 seconds in ms
timeout: 20_000,
},
);
};

waitFor will also throw an error if the actor reaches a final state before satisfying the condition. Read more about final states.