Using State Machine to build your React app
Tags: styledcomponents, xstate, nextjs, react
Published: 2020-05-07 00:46:44

---
title: Using State Machine to build your React app
published: true
description: Using state machine (xState) to hook up your app!
tags: styledcomponents, xstate, nextjs, react
---

I hope this article finds you healthy and safe. 

With the coronavirus causing chaos all over the world, I thought it would be useful to build an app that shows the latest metrics of each individual country cases. I will be using Next JS, styled-components and state machines!

## Why state machine?

When I'm building an app in React, I will have a problem trying to understand how that component would work or react when a condition happens. State machine helps me structure my app into states, transitions and events so that my app becomes more predictive and eliminates any unexpected bugs or states.

In short, xState makes our code cleaner and maintainable in the long run! Believe me.

Read [this article](https://dev.to/davidkpiano/redux-is-half-of-a-pattern-1-2-1hd7) by the [author of xState](https://twitter.com/davidkpiano) himself to understand more on state machines.

You can check out the finished repo at https://github.com/nazmifeeroz/covid-xstate-next and view the finished app deployed here, https://covid-next.now.sh/

Let's begin coding!

## Setting up your app

I will be using [Next.js](https://nextjs.org) to bootstrap the app. In your terminal, run:

```shell
$ mkdir covid-xstate-next && cd covid-xstate-next && npm init -y
```

That should initialise `npm` in your folder, which you can then install the required packages:

```shell
$ yarn add react react-dom next xstate @xstate/react styled-components
```

Once installed, create a new folder `pages` and a file called `index.js`:

```shell
$ mkdir pages && touch pages/index.js
```

Open up `package.json` in your code editor and replace the `test` script to this:

```json
  "scripts": {
    "dev": "next"
  }
```

This will be the command to run your app. Before we can run, let's add some template in `index.js`:

```javascript
import React from 'react'

const IndexPage = () => (
  <div>CoronaVirus Information</div>
)

export default IndexPage
```

Now you can run `yarn dev`, and you should be able to open up your app in your browser at `http://localhost:3000` and you should see your browser showing the texts we added from `index.js`.

## The State Machine (The Brain)

Now that we are all set, let's dive into building the **brain** in our app!

We'll start by setting up the state chart of our app. In your `index.js` file, add this before your `IndexPage` function:

```javascript
// pages/index.js
import { Machine } from 'xstate'

const statsMachine = Machine({
  id: 'statsMachine',
  initial: 'fetchStats',
  states: {
    fetchStats: {}
  }
})
```

Here we initialise the machine by defining the initial state of the app which will be `fetchStat`. In layman terms, when the page is loaded, we want the app to fetch stats first! Pretty straight forward right?

In xState, we can run an asynchronous function that returns a promise. Whether it is resolved or rejected, we can define the transition it to the next state accordingly.

We will be using an [open sourced api](https://github.com/javieraviles/covidAPI) to retrieve the stats. Within the `fetchStats` state, we will call the `invoke` attribute which will fetch the data from the api:

```javascript
// pages/index.js
import { Machine } from "xstate"

const statsApi = "https://coronavirus-19-api.herokuapp.com/countries"
const statsMachine = Machine({
  id: "statsMachine",
  initial: "fetchStats",
  states: {
    fetchStats: {
      invoke: {
        src: () =>
          new Promise(async (resolve, reject) => {
            try {
              const stats = await fetch(statsApi).then((response) =>
                response.json()
              )
              return resolve(stats)
            } catch (error) {
              console.log("error in fetching stats: ", error)
              return reject(error)
            }
          }),
      },
    },
  },
})
```

The `invoke` attribute takes in a `src` which will be the function that will run a promise function. To get the resolved data or rejected error, we can get it from the `onDone` and `onError` attribute respectively:

```javascript
// pages/index.js
import { assign, Machine } from 'xstate'

const statsApi = 'https://coronavirus-19-api.herokuapp.com/countries'

const statsMachine = Machine({
  id: 'statsMachine',
  initial: 'fetchStats',
  states: {
    fetchStats: {
      invoke: {
        src: () => new Promise((resolve, reject) => {
          try {
            const stats = 
              await fetch(statsApi)
                .then(response => response.json())

            return resolve(stats)
          } catch (error) {
            console.log('error in fetching stats: ', error)
            return reject(error)
          }
        }),
        onDone: { target: 'ready', actions: 'assignStats' },
        onError: 'error',
      }
    },
    ready: {},
    error: {}
  }
})
```

As you might have guessed, when the promise fetches successfully, it resolves with the data and transits via the `onDone` attribute. The target is `ready` which is a state and waits there for the next event. If the promise returns error, it gets rejected and transits to the `error` state via the `onError` attribute. 

Now if you notice, we have another attribute within the `onDone` which is the `actions` attribute. What that does is when the Promise resolves successfully, we want to `assign` the data into the `context` of the machine.

```javascript
// pages/index.js
import { assign, Machine } from 'xstate'

const statsApi = 'https://coronavirus-19-api.herokuapp.com/countries'

const statsMachine = Machine({
  id: 'statsMachine',
  initial: 'fetchStats',
  context: {
    stats: null
  },
  states: {
    fetchStats: {
      invoke: {
        src: () => new Promise((resolve, reject) => {
          try {
            const stats = 
              await fetch(statsApi)
                .then(response => response.json())

            return resolve(stats)
          } catch (error) {
            console.log('error in fetching stats: ', error)
            return reject(error)
          }
        }),
        onDone: { target: 'ready', actions: 'assignStats' },
        onError: 'error',
      }
    },
    ready: {},
    error: {}
  }
},
{
  actions: {
    assignStats: assign((_context, event) => ({
      stats: event.data
    }))
  }
})
```

In xState, we can define out actions into another object so that our machine object won't be so cluttered. In the `assignStats` action, we use the `assign` function that takes in the latest `context` and `event` that was passed from the resolved promise `data` and we store it in the `stats` prop.

Now we're done with the brain of our app! Let's move to the render function (the body).

## The Body (Main Render function)

Now back to our JSX function, we want to show loading when the app is in `fetchStats` state. Then show the stats whens it's done at `ready` state.

```javascript
// pages/index.js
import { assign, Machine } from "xstate"
import { useMachine } from "@xstate/react"

const statsApi = "https://coronavirus-19-api.herokuapp.com/countries"

const statsMachine = Machine({
  // … our machine object
})

const IndexPage = () => {
  const [current, send] = useMachine(statsMachine)

  return (
    <>
       <div>CoronaVirus Information</div> 
      {current.matches("fetchStats") && <div>Loading Stats…</div>} 
      {current.matches("error") && <div>Error fetching stats…</div>} 
      {current.matches("ready") && <div>Stats loaded!</div>} 
    </>
  )
}

export default IndexPage
```

We used the `useMachine` hook to _translate_ the `statsMachine` that return an array. The first element `current` will store all our machine details, on which state we are in and the `context` available we can use. When the current state is `fetchStats`, we show a loading component. When the current state is `ready`, we show the stats! You can imagine the possibilities when we have more states which we can then simply call the `current.matches` function. 

This makes our code much cleaner and more understandable, making our app more maintainable. No more cluttered boolean states like `isLoading`, `isFetching` or `hasError`!

Now, lets create components for each individual states. We can put our components into its own folder under src. In our root project folder, run:

```shell
$ mkdir -p src/components && touch src/components/CountrySelector.js && touch src/components/stat.js && touch src/components/CountrySearch.js
```

The `CountrySelector` component will show all the countries available in a dropdown box:

```javascript
// src/components/CountrySelector.js
import React from "react"
import styled from "styled-components"

const CountrySelector = ({ handleChange, stats }) => (
  <div>
    <Selector onChange={handleChange}>
      <option>Select a country</option>
      {stats.map((stat, i) => (
        <option key={`${stat.country}-${i}`}>{stat.country}</option>
      ))}
    </Selector>
  </div>
)

const Selector = styled.select`
  -webkit-box-align: center;
  align-items: center;
  background-color: rgb(255, 255, 255);
  cursor: default;
  display: flex;
  flex-wrap: wrap;
  -webkit-box-pack: justify;
  justify-content: space-between;
  min-height: 38px;
  position: relative;
  box-sizing: border-box;
  border-color: rgb(204, 204, 204);
  border-radius: 4px;
  border-style: solid;
  border-width: 1px;
  transition: all 100ms ease 0s;
  outline: 0px !important;
  font-size: 15px;
  margin-bottom: 10px;
`

export default CountrySelector
```

The `CountrySelector` component will receive the `stats` data to show in a dropdown box and the `handleChange` function which will pass the selected country back to our machine to show the stat of the country.

Next the `CountrySearch` component will allow user to search for a specific country. It receives the prop `handleChange` to update the machine for the country user has input.

```javascript
// src/components/CountrySearch.js
import React from 'react'

const CountrySearch = ({ handleChange }) => {
  return (
    <input
      onChange={handleChange}
      placeholder="Search for a country"
      type="search"
    />
  )
}

export default CountrySearch
```

Now for our last component `stat` will format and display the country stat:

```javascript
// src/components/stat.js
import React from 'react'

const Stat = ({ stats }) => {
  return stats.map((stat, i) => (
    <div key={`${stat.country}-${i}`}>
      <br />
      <b>{stat.country}</b>
      <br />
      Cases: {stat.cases} | Today: {stat.todayCases} | Active: {stat.active}{' '}
      <br />
      Deaths: {stat.deaths} | Recovered: {stat.recovered} | Critical:{' '}
      {stat.critical}
    </div>
  ))
}

export default Stat
```

We can now update our `pages/index.js` page to have all the components and pass its props.

```javascript
// pages/index.js
import React from "react"
import { assign, Machine } from "xstate"
import { useMachine } from "@xstate/react"
import CountrySelector from "../src/components/CountrySelector"
import Stat from "../src/components/stat"
import CountrySearch from "../src/components/CountrySearch"

const statsApi = "https://coronavirus-19-api.herokuapp.com/countries"
const statsMachine = Machine({
  // … our machine object
})

const IndexPage = () => {
  const [current, send] = useMachine(statsMachine)
  return (
    <>
       <h3>CoronaVirus Information</h3> 
      {current.matches("fetchStats") && <div>Loading Stats…</div>} 
      {current.matches("error") && <div>Error fetching stats…</div>} 
      {current.matches("ready") && (
        <>
           
          <CountrySelector
            stats={current.context.stats}
            handleChange={(country) => send("COUNTRY_SELECTED", { country })}
          />
           
          <CountrySearch
            handleChange={(country) => send("COUNTRY_SELECTED", { country })}
          />
           
        </>
      )}
       
      {current.context.countriesSelected.length > 0 && (
        <Stat stats={current.context.countriesSelected} />
      )}
       
    </>
  )
}
export default IndexPage
```

We have not added the event for `COUNTRY_SELECTED` and the context for `countriesSelected` in our machine. Lets do that now:

```javascript
const statsMachine = Machine(
  {
    id: "statsMachine",
    initial: "fetchStats",
    context: {
      countriesSelected: [],
      stats: null,
    },
    states: {
      fetchStats: {
        invoke: {
          src: () =>
            new Promise(async (resolve, reject) => {
              try {
                const stats = await fetch(statsApi).then((response) =>
                  response.json()
                )
                return resolve(stats)
              } catch (error) {
                console.log("error in fetching stats: ", error)
                return reject(error)
              }
            }),
          onDone: { target: "ready", actions: "assignStats" },
          onError: "error",
        },
      },
      ready: {
        on: {
          COUNTRY_SELECTED: { actions: "updateSelectedCountry" },
        },
      },
      error: {},
    },
  },
  {
    actions: {
      assignStats: assign((_context, event) => ({
        stats: event.data,
      })),
      updateSelectedCountry: assign((context, event) => ({
        countriesSelected: context.stats.reduce(
          (acc, stat) =>
            stat.country
              .toLowerCase()
              .match(event.country.target.value.toLowerCase())
              ? [...acc, stat]
              : acc,
          []
        ),
      })),
    },
  }
)
```

What we've just added here is whenever the `CountrySelector` or `CountrySearch` sends a new input by the user, it calls the `COUNTRY_SELECTED` event. This event calls upon the `updateSelectedCountry` action which will update the countries stats to display by the `Stat` component!

One of the many benefits I love about state machine is that your component gets decoupled from its logic and the UI. It also helps us have a clearer picture when we code, on what had happened, is happening and going to happen when user does this or that.

I hope this article helps to paint a good picture on why xState will make you code cleaner and maintainable on the long run!

Cheers! Happy coding!