Did you know?
You can export a variable and a type with the same name in typescript โ๏ธ
You can then use it both as a type and value with a single import ๐ช
Suggested with zod and Effect schema ๐
It's common to have some outside data required to initialize the machine context.
This is the purpose of the input value in XState:
interface Input defines the type of the required input
The Input function take the input as value and returns the new context
๐ก Important: The return type of Input is a Promise. This is where we need to use Actors (more details below ๐)
Events
events.ts is a union of types for all possible events in the machine.
๐ก All events contain a readonly type, used by XState to distinguish between events (Discriminated Unions)
Note: The xstate.init event is automatically sent by the machine itself when started.
We are going to use this event to initialize the machine context from Input.
Actions
actions.ts contains the functions to execute for each event.
Note: An assign function returns Partial<Context.Context> (sync updated context).
When an assign action returns a Promise instead we need to use Actors.
There is more.
Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.
Implement a state machine in XState
We build the final state machine inside machine.ts:
Import and use context, input, events, and actions
Create the machine using setup
Create actors using fromPromise
Use Actors to initialize context from Input
We use an actor to initialize the context from some input (Input) with an async function (Promise).
An actor is created using invoke:
Adding invoke at the root of the machine will start an actor as soon as the machine starts.
The actor will be active for the lifetime of its parent machine actor.
Inside src we define the specific actor to invoke:
The actors are defined inside setup.
In this example onInit is an actor created using fromPromise that returns Context.Context and requires Input.Input as parameter.
The actor calls Input.Input to initialize the context:
Communication between actors with events
The machine and its child actor communicate by passing events.
Inside invoke we define a input value. Inside it we listen for the xstate.init event. This event contains the Input.Input parameter required to execute the actor:
We then also define onDone that will be executed when the Promise completes. Inside it we use assign to read the output of the actor and update the context:
Invoking actors inside states
The same logic can be used to execute assign actions inside a state.
In this example the onAddEvent action updates the context with an async function:
We therefore need to invoke an actor that executes the async logic.
We start by defining a new state that the machine enters after add-event:
The AddingLines state defines invoke to create a new actor:
The actor is defined inside setup. It used fromPromise to execute the onAddEvent function:
Finally, we use onDone to update the context and come back to the Idle state:
This is how you organize a state machine and use actors in XState v5.
You can use a similar logic to execute more complex actors and machines (using createMachine, fromTransition, fromObservableand more).
If you are interested to learn more, every week I publish a new open source project and share notes and lessons learned in my newsletter. You can subscribe here below ๐
Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.