An Introduction to XState in TypeScript
Tags: xstate, typescript, javascript
Published: 2021-02-04 13:45:06

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ppymj2f0c9dw8rrcfzc0.png)
When systems and programs are small, state management is usually rather simple, and it’s easy to envision the status of the application and the various ways in which it can change over time. It’s when we scale and our applications become more complex that challenges arise. As systems grow larger, it’s vital to not just have a plan for state management, but a vision for how the entire system functions. This is where state machines come into play and can offer a comprehensive solution to state management by helping us model our application state.

State machines allow us to build structured and robust UIs while forcing us, as developers, to think through each and every state our application could be in. This added insight can enhance communication not only among developers, but also between developers, designers, and product managers as well.

## What are statecharts and state machines?
A finite state machine is a mathematical system that can only ever be in one of a finite number of defined states. A traffic light is a simple example. A traffic light only has four states that it could ever be in: one for each of its three lights (red, yellow, and green) being on while the other two lights are off. The fourth is an error state where the traffic light has malfunctioned.

Statecharts are used to map out the various states of a finite system, similar to a basic user flow chart. Once the finite number of states are determined, transitions — the set of events that move us between each state — are defined. The basic combination of states and transitions are what make up the machine. As the application grows, new states and transitions can be added with ease. The process of configuring the state machine forces us to think through each possible application state, thus clarifying the application’s design.

[XState](https://xstate.js.org/) is a library developed by David Khourshid that provides us with the ability to create and run state machines in JavaScript/TypeScript, along with a thorough and easily navigated set of documentation. It also provides us with the XState visualizer, which allows both technical and non-technical people to see how we can move through the finite set of states for a given system, thus providing [“a common language for designers and developers.”](https://www.youtube.com/watch?v=VU1NKX6Qkxc&t=780s)

## Using TypeScript — Context, Schema, and Transitions

We can also type our XState machine using TypeScript. XState works nicely with TypeScript because XState makes us think through our various application states ahead of time, allowing us to clearly define our types as well.

XState `Machine` instances take two object arguments, `configuration` and `options`. The `configuration` object is the overall structure of the states and transitions. The `options` object allows us to further customize our machine, and will be explained in depth below.

```ts
  const xStateMachine = Machine<Context, Schema, Transitions>(
    xStateConfig,
    xStateOptions
  );
```

The three type arguments that we use to compose our machine are `schema`, `transitions`, and `context`. They help us describe every possible state, map out how we move from state to state, and define all the data that can be stored as we progress through the machine. All three are fully defined before the machine is initialized:

* **Schema** is an entire overview of the map of the machine. It defines all of the states that the application could be in at any given moment.
* **Transitions** are what allow us to move from state to state. They can be triggered in the UI by event handlers. Instead of the event handlers containing stateful logic, they simply send the type of the transition along with any relevant data to the machine, which will then transition to the next state according to the `schema`.
* **Context** is a data store that is passed into your state machine. Similar to [Redux](https://react-redux.js.org/), context represents all the data potentially needed at any point in the lifecycle of your program as it moves from state to state. This means that while we may not have all the actual data upon initialization, we do need to define the shape and structure of our `context` data store ahead of time.

Let’s take some time to look at the initial configuration of a state machine:

```ts
const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
  id: "Email Application",
  initial: "HOME_PAGE",
  context: {},
  states: {}
 };
 ```

* **ID** is a string that refers to this specific machine.
* **Initial** refers to the initial state of the machine.
* **Context** is an object that defines the initial state and shape of our `context` data store, similar to initial state in [Redux](https://react-redux.js.org/). Here, we set out all the potential pieces of state data as the keys in this object. We provide initial values where appropriate, and unknown or possibly absent values can be declared here as `undefined`.

Our machine has all the information it needs to initialize, we have mapped out the various states of the machine, and the gears of our machine are moving. Now let’s dive into how to utilize the various tools provided by XState to trigger transitions and handle data.

## States
To illustrate how XState helps us manage application state, we’ll build a simple example state machine for an email application. Let’s think of a basic email application where, from our initial `HOME_PAGE` state (or welcome screen), we can transition into an `INBOX` state (the screen where we read our emails). We can define our schema with these two states and define a transition called `OPEN_EMAILS`.

```ts
interface Schema {
 states: {
  HOME_PAGE: {};
  INBOX: {};
 };
};

type Transitions = { type: "OPEN_EMAILS" };

const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
 id: "Email Application",
 initial: "HOME_PAGE",
 context: initialContext,
 states: {
  HOME_PAGE: {
    id: "HOME_PAGE",
    on: { OPEN_EMAILS: "INBOX" },
  },
  INBOX: {
   id: "INBOX",
  }
 }
};
```

With our two states and transition defined, it is clear to see how our state machine begins in the `HOME_PAGE` state and has its transition defined in the `on` property.

## Options

**1. Services + Actions**

We now have a state machine with a basic transition, but we haven’t stored any data in our `context`. Once a user triggers the `OPEN_EMAILS` transition, we will want to invoke a `service` to fetch all the emails for the user and use the `assign` action to store them into our `context`. Both of these are defined in the options object. And we can define emails within our `context` as an optional array since upon initialization of the machine we haven't yet fetched any emails. We will have to add two new states to our schema: a `LOADING_EMAILS` pending state and an `APPLICATION_ERROR` error state, if this request fails. We can invoke this request to fetch the emails in our new `LOADING_EMAILS` state.

```ts
type Context = {
 emails?: [];
};

const initialContext: Context = {
 emails: undefined,
};

interface Schema {
 states: {
  HOME_PAGE: {};
  LOADING_EMAILS: {};
  INBOX: {};
  APPLICATION_ERROR: {};
 };
};

type Transitions = { type: "OPEN_EMAILS"}

const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
 id: "Email Application",
 initial: "HOME_PAGE",
 context: initialContext,
 states: {
  HOME_PAGE: {
   on: { OPEN_EMAILS: "LOADING_EMAILS" },
  },
  LOADING_EMAILS: {
   invoke: {
    id: "LOADING_EMAILS",
    src: (context, event) => 'fetchEmails',
    onDone: {
     actions: 'setEmails',
     target: "INBOX",
    },
    onError: {
     target: "APPLICATION_ERROR",
    },
   },
  },
  INBOX: {
   id: "INBOX",
  },
  APPLICATION_ERROR: {
   after: {
    5000: `HOME_PAGE`,
   },
  },
 },
};

const xStateOptions: Partial<MachineOptions<Context, any>> = {
  services: {
    fetchEmails: async () =>  {
      return new Promise<void>((resolve, reject) =>{
        resolve();
        // reject();
      })
    },
  },
  actions: {
    setEmails: assign({ emails: (context, event) => event.data }),
  }
}

const xStateMachine = Machine<Context, Schema, Transitions>(
  xStateConfig,
  xStateOptions
);
```

The four keys in the configuration for `invoke` are `id`, `src`, `onDone`, and `onError`, with the `id` being an identifier for the invocation. The `src` is the function `fetchEmails` that returns our promise containing the email data. Upon a successful fetch, we will move into `onDone`, where we can use the `assign` action to store the email data returned from our fetch in our `context` using the `setEmails` action. As you can see, the two arguments to `fetchEmails` are `context` and `event`, giving it access to all the `context` and `event` values. We also have to let our machine know where to go next by providing a target state, which in this instance is our `INBOX`. We have a similar structure for a failed fetch, in which our target is an error state, `APPLICATION_ERROR`, that returns to the `HOME_PAGE` state after five seconds.

**2. Guards**

Conditional state changes can be handled by the use of guards, which are defined in the `options` object. Guards are functions that, once evaluated, return a boolean. In XState, we can define this guard in our transition with the key cond.

Let’s add another state for drafting an email, `DRAFT_EMAIL`. If a user was previously drafting an email when the application successfully fetches email data, the application would take the user back to the `DRAFT_EMAIL` page instead of the `INBOX`. We’ll implement this conditional logic with an `isDraftingEmail` function. If the user was in the process of drafting an email when data was successfully fetched, `isDraftingEmail` will return `true` and send the machine back to the `DRAFT_EMAIL` state; if it returns `false`, it will send the user to the `INBOX` state. Our guard will be handled in a new state called `ENTERING_APPLICATION` that will be responsible for checking this condition. By using the `always` key when defining this state, we tell XState to execute this conditional logic immediately upon entry of the state.

```ts
const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
  id: "Email Application",
  initial: "HOME_PAGE",
  context: initialContext,
  states: {
    HOME_PAGE: {
      on: { OPEN_EMAILS: "LOADING_EMAILS" },
    },
    LOADING_EMAILS: {
      invoke: {
        id: "LOADING_EMAILS",
        src: 'fetchEmails',
        onDone: {
          actions: 'setEmails',
          target: "ENTERING_APPLICATION",
        },
        onError: {
          target: "APPLICATION_ERROR",
        },
      },
    },
    ENTERING_APPLICATION: {
      id: "ENTERING_APPLICATION",
      always:[
        {
          target: "DRAFT_EMAIL",
          cond: 'isDraftingEmail',
        },
        { target: "INBOX" }
      ]
    },
    INBOX: {
      id: "INBOX",
    },
    DRAFT_EMAIL: {
      id: "DRAFT_EMAIL",
    },
    APPLICATION_ERROR: {
      after: {
        5000: `HOME_PAGE`,
      },
    },
  },
}

const xStateOptions: Partial<MachineOptions<Context, any>> = {
  services: {
    fetchEmails: async () =>  {
      return new Promise<void>((resolve, reject) =>{
        resolve();
        // reject();
      })
    },
  },
  actions: {
    setEmails: assign({ emails: (context, event) => event.data }),
  },
  guards: {
    isDraftingEmail: () => {
      return true;
      // return false;
    }
  }
}

const xStateMachine = Machine<Context, Schema, Transitions>(
  xStateConfig,
  xStateOptions
);
```

## XState Visualizer

One of XState’s best features is the XState visualizer, which takes in our machine configuration and automatically provides an interactive visual representation of our state machine. These visualizations are how [“state machines provide a common language for designers and developers.”](https://www.youtube.com/watch?v=VU1NKX6Qkxc&t=780s)

A final look at our XState visualizer shows us the map of our entire email application. Use either link below to test out our machine in a new tab! Once the sandbox is loaded in a new tab, it should open a second new tab with the visualizer. If you don't see the visualizer, disable your pop-up blocker and refresh the sandbox.

In the visualizer, click on the `OPEN_EMAILS` transition to run the state machine. To change the outcome of the machine, comment/uncomment the return values in the `fetchEmails` and `isDraftingEmails` functions in the Sandbox.


{% codesandbox 7xq10 %}

[XState Email Application Visualizer](https://codesandbox.io/s/xstate-email-application-visualizer-7xq10?file=/src/index.ts)

## Conclusion

XState provides a high level understanding of our application via its schema and visualizer, while still offering more granular visibility and control of state and data through its configuration. Its usability helps us tame complexity as our application grows, making it an excellent choice for any developer. Thank you so much for reading and keep an eye out for part II: XState and React!