Mastering XState Fundamentals: A React-powered Guide
Tags: react, state, javascript, xstate
Published: 2024-04-16 13:41:50

The first article in this series discussed the core differences between the reducer and finite state machine models. In this article, we will dig into the fundamental concepts of XState using the React framework. We'll cover states, events, actions, and guards — providing a solid foundation for building programs with XState.
XState is a robust state management library that leverages the simplicity and power of finite state machines to build efficient, scalable JavaScript or TypeScript applications.
Before we begin, prior knowledge of JavaScript and React is necessary to grasp the content of this article. Familiarity with Redux or React's `useReducer()` hook would be beneficial but is not mandatory. Lastly, we will be using JavaScript throughout this article.
If you wish to code along, you will need XState and @xstate/react in addition to React.

```bash
npm i xstate @xstate/react
```
Now that we’re on the same page, let us learn about state.

## State

### Reducer model
When we create apps using the reducer pattern, we need to keep all the data in one spot, called the state. For example, if we're making an API call program, it will include the `status`, `data`, and `error`.

```javascript
const initialState = {
  status: "idle",
  data: undefined,
  error: undefined,
};
```
These variables will change throughout the program's lifecycle. When the program is triggered, it transitions from `idle` to `loading`. If the API call succeeds, it assigns the returned value to the `data` property and changes the status from `loading` to `success`. Otherwise, it attaches the returned error message to the `error` property and changes the `status` from `loading` to `error`.

```javascript
const fsmReducer = (state, action) => {
  switch (state.status) {
    case "idle": {
		   //code to move to loading state when triggered.
    }
    case "loading": {
	    //move to success if the api call is successful. Otherwise, move to error state.
    },
    case "success" {},
    case "error" {},
     default: {
      return state;
    }
  }
};
```
In a usual reducer model, we typically switch based on events, not states. However, in the example mentioned, we're switching based on the state, aligning with the principles of finite state machines.
In summary, the reducer model stores all application variables in a central store, while the finite state machine pattern takes a different approach.

### Finite State Machines Model
The FSMs pattern distinguishes clearly between the application's status and other data. In the FSMs model, the program's status is referred to as the state, representing the qualitative aspect, while the remaining data represents the quantitative part of the application.

A program can exist in only one state at any given time and has a finite number of possible states. For instance, a light bulb switch can be either in the `on` or `off` state.

All other data concerning the application constitutes the quantitative part. For our API call logic, this could be the resolved data from the API call or error message.

Let's redefine the API call program as in the previous example, this time utilizing FSMs with XState.

```javascript
import { createMachine } from "xstate";

const fetcher = createMachine({
  initial: "idle",
  context: {
    data: undefined,
    error: undefined
  }
  states: {
    idle: {
	    //Some code to execute
    },
    loading: {
	    //more code
    },
    success: {
	    //more code
    },
    error: {
	    //more code
    },
  },
});
```
In XState, states are explicitly defined, and all other data is stored in the context object.

The `createMachine()` function accepts the application’s logic object as an argument.

You need to specify the initial state of the application, especially if it has multiple states. In this case, its initial state is `idle`.

## Events
Events are occurrences within the system, which can be triggered by a user action, like clicking a button, or by the program itself, such as resolving or rejecting a promise in JavaScript.

### Sending Events
When a user interacts with the UI, like clicking a button or typing, we aim to capture these events and respond by executing some code. XState provides a method to capture these UI events and send them to the state machine for proper handling. Additionally, these events can carry additional dynamic information from the UI.

For example, consider a React counter machine. Whenever the user clicks a button, we want to increment the count by a number provided by the UI, 2 in this case.

```javascript
import { assign, createMachine } from "xstate";
import { useMachine } from "@xstate/react";

const counterMachine = createMachine({
  context: {
    count: 0,
  },
  on: {
    ADD: {
     //code to handle the Add event
    },
  },
});

export default function App() {
  const [snapshot, send] = useMachine(counterMachine);

  return (
    <div>
      <p>count: {snapshot.context.count}</p>
      <button onClick={() => send({ type: "ADD", data: 2 })}>Add two</button>
    </div>
  );
}
```
XState provides the `useMachine()` React hook, which returns an array containing the snapshot object and the send function. The snapshot object holds essential details about the state machine, including the context object. Meanwhile, the send function allows us to dispatch events from the UI to the machine for appropriate handling.

Next, we would see how events such as `ADD` are handled within the machine’s logic.

### Handling Events
All state- or program-level events are defined within the `on` object.

For single-state programs like the counter example, the `on` object is directly placed on the program logic object.

For multi-state applications like the API-call program, the `on` object resides within individual states.

It's worth noting that even if an application has multiple states, you can still have an `on` object with events directly on the program logic object, similar to the counter example. When such top-level events are dispatched to the machine, they take effect regardless of the current state of the application.

```javascript
const fetcher = createMachine({
  initial: "idle",
  context: {
    data: undefined,
    error: undefined
  },
  states: {
    idle: {
      on: {
        FETCH: {
          target: "loading",
        },
      },
    },
    loading: {
      on: {
        FETCHED: {
          target: "success",
        },
        CANCEL: {
          target: "idle",
        },
      },
    },
    success: {
     on: {
       RELOAD: {
         target: "loading",
       },
     },
    },
  },
});
```
The `idle` state allows the `FETCH` event, while the `loading` state permits the `FETCHED` and `CANCEL` events.

Each of these events is assigned an action object. In this case, the action object contains the `target` property, specifying the transition or the next state the machine will enter when the event is received.

If the `target` is not specified, the program will stay in its current state. Apart from transitions, the action object can also include `actions`, which we'll explore next.

## Actions
In addition to transitions, actions are also responses to events. `actions` can include invoking a function, altering a variable within the context object, or executing asynchronous operations. Next, we’ll learn about each of these common actions in XState.

### Changing a Variable
In our counter app example, we utilize the assign action to modify variables within the context object. The syntax is simple and allows us to modify one or multiple variables simultaneously.

```javascript
const counterMachine = createMachine({
  context: {
    count: 0,
    otherVariable: undefined
  },
  on: {
    ADD: {
      actions: assign({
        count: ({ context, event }) => context.count + event.data,
        otherVariable: ({context, event}) => //new value
      }),
    },
  },
});
```

### Invoking a Function
When an event is dispatched, you might simply want to execute a function, as demonstrated in this case:

```javascript
const counterMachine = createMachine({
  context: {
    count: 0,
  },
  on: {
    ADD: {
      actions: ({event}) => console.log(event)
    },
  },
});
```
The above action will merely log the event. Additionally, you can execute multiple actions sequentially by assigning an array of action functions to the actions property.

```javascript
const counterMachine = createMachine({
  context: {
    count: 0,
  },
  on: {
    ADD: {
      actions: [({event}) => console.log(event),
	      assign({
		count: ({event}) => event.data
            })
      ]
    },
  },
});
```

### Performing Asynchronous Operations
XState offers convenient methods to handle asynchronous processes using actors. In this discussion, we'll concentrate on the `fromPromise` actor, designed for executing promises, including API calls using `fetch()`.

Asynchronous operations via actors have a slight distinction from other actions. While other actions are accessible on the event level within the `actions` object, actors (employed for asynchronous tasks in XState) are executed via the `invoke` object, which is only available at the state level.

```javascript
import { assign, createMachine, fromPromise } from "xstate";
import { useMachine } from "@xstate/react";

const counterMachine = createMachine({
  initial: "idle",
  context: {
    data: undefined,
    error: undefined,
  },
  states: {
    idle: {
      on: {
        LOAD_DATA: {
          target: "loading",
        },
      },
    },
    loading: {
      invoke: {
        src: fromPromise(() =>
          fetch("https://fakestoreapi.com/products/category/jewelery").then(
            (res) => res.json()
          )
        ),
        onDone: {
          target: "success",
          actions: assign({
            data: ({ event }) => event.output,
          }),
        },
        onError: {
          target: "error",
          actions: assign({
            error: ({ event }) => event.error,
          }),
        },
      },
    },
    success: {},
    error: {},
  },
});

export default function App() {
  const [snapshot, send] = useMachine(counterMachine);

  return (
    <div>
      <button onClick={() => send({ type: "LOAD_DATA" })}>Load Data</button>

      {snapshot.value == "loading" && <div>Loading...</div>}

      {snapshot.value == "success" && (
        <ul>
          {snapshot.context.data.map((item) => (
            <li key={item.id}>{item.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```
When the user clicks on the `Load Data` button, it triggers the `loading` state, where the `invoke` object is executed. We assign the `fromPromise()` to the `src` property. The `fromPromise()` actor requires a callback function that returns a fulfilled or rejected promise. If the promise succeeds, the `onDone` event is triggered; otherwise, the `onError` event is called.

## Guards
If you need to transition to a particular state or execute certain actions based on the value of a variable within your context object, you can achieve this functionality using guards. Guards are functions that evaluate to either `true` or `false`.

Instead of assigning a single action object with transitions and actions, you attach an array of action objects. Each object in this array will include a guard function along with `target` and `actions`.

XState executes these objects serially. The first object whose guard evaluates to true will have its `target` and `actions` executed.

```javascript
const counterMachine = createMachine({
  initial: "editing",
  context: {
    todo: "",
  },
  states: {
    editing: {
      on: {
        RECORD_INPUT: {
          actions: assign({
            todo: ({ event }) => event.data,
          }),
        },
        SAVE: [
          {
            guard: ({ context }) => context.todo.length > 0,
            actions: () => console.log("saved"),
          },
          {
            guard: ({ context }) => context.todo.length == 0,
            actions: () => console.log("not saved"),
          },
        ],
      },
    },
  },
});
```
In this simple todo app, the user is unable to save an empty text or todo.

## Conclusion
This article explores the basics of state machines using XState. It covers the fundamentals of states, events, actions, and guards. Each of these concepts has more depth to explore, including parallel states, entry and exit actions, and input and output, all of which you learn more about in the documentation.

XState is a powerful library with [comprehensive documentation](https://stately.ai/docs). Keeping the documentation handy while building your next app with XState will be invaluable.