State Management Made Easy: React Native and XState

State Management Made Easy: React Native and XState image

React state management is hard, but it's not as hard if you use XState, a JavaScript library that helps you manage the state of your application. It also enables you to visualize state charts with the XState Visualizer, so you can declaratively describe your app's behavior, from its components to the overall app logic.

This article will explain what a finite state machine is and demonstrate a React app that uses XState to manage state. My name is Lucas Dutra, and I'm a Brazilian developer with five years of experience in the React ecosystem. If you want to learn the basics of XState and its integration with React Native, keep reading.

What Is a Finite State Machine?

A finite state machine is a mathematical model of computation that describes the behavior of a system that can be in only one state at any given time.

For example, a state machine can represent you with a finite number of two states: asleep or awake. At any given time, you're either asleep or awake. You can't be in both states at the same time, just as you can't be neither asleep nor awake.

Formally, a finite state machine has five parts:

  1. An initial state
  2. A finite number of states
  3. A finite number of events
  4. A transition function to determine the next state for the current state and event
  5. A (possibly empty) set of final states

Finite State Machines - XState Docs

Starting the Project

Initialize a project:

npx create react-native-app rn-xstate
cd rn-xstate

Install the XState lib dependency:

yarn add xstate@latest @xstate/react --save

Before we dig deep into the code of it all, we need to understand a few of the properties of the machine configuration object that the createMachine method receives:

const walkWithDogMachine = createMachine({
  id: 'walkWithDogMachine',
  initial: 'stopped',
  context: {
    someInfo: ''
  },
  states: {
    walking: {},
    stopped: {},
  }
});
  • id: the identifier of the machine
  • initial: the initial state of the machine
  • states: the states of the machine, in this case walking and stopped
  • context: an object that holds some information

Let's now add transitions to the machine. Transitions define how the machine reacts to events. We put transitions inside the on object.

const walkWithDogMachine = createMachine({
  id: 'DogMachine',
  initial: 'stopped',
  states: {
    walking: {
        on: {
            STOPPED: 'stopped',
            REST: 'rest'
        }
    },
    stopped: {
        on: {
            WALKING: 'walking',
            REST: 'rest'
        }
    },
    rest: {
        type: 'final'
    }
  }
});

Now, the machine starts with an initial state called stopped. When the machine receives a WALKING event, it will transition to the walking state. When it receives a REST event, it will transition to the rest state.

The rest state has a type called final, which is the end state of the state machine.

Creating the React Native App

Let's now create an app that fetches a joke from an endpoint. Let's create a file called jokeMachine under src/machines for the state machine.

import { createMachine, assign } from 'xstate';
​
function invokeFetchJoke() {
  return fetch('http://api.icndb.com/jokes/random')
    .then((response) => response.json())
    .then(data => data.value.joke)
    .catch(error => error)
}
​
​
const jokeMachine = createMachine({
  id: 'jokeMachine',
  initial: 'idle',
  context: {
    joke: ''
  },
  states: {
    idle: {},
    loaded: {},
    failed: {},
    loading: {
      invoke: {
        id: 'invoke-fetch-joke',
        src: invokeFetchJoke,
        onDone: {
          target: 'loaded',
          actions: assign({
            joke: (_, event) => event.data
          })
        },
        onError: 'failed'
      }
    }
  },
  on: {
    FETCH_JOKE: 'loading'
  }
});
​
export default jokeMachine

Here's how this code works: the invokeFetchJoke is a function that calls the endpoint and returns a promise.

We have a state machine that starts and ends with the state idle and has only one property in the context, called joke, i.e. the joke that we will fetch from the endpoint.

In the states object we have four states:

  • idle
  • loaded
  • failed
  • loading

The only state that does something is loading. When that state is triggered with the FETCH_JOKE transition, the machine invokes the invokeFetchJoke function. From that point, we have two possibilities:

  • The fetch fails and we transition to the state failed mapped in onError.
  • The fetch succeeds, updates the joke key inside the context with the assign function, and the machine transitions to the loaded state.

App.js:

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { ActivityIndicator, StyleSheet, Text, TouchableHighlight, View } from 'react-native';
import jokeMachine from './src/machines/jokeMachine'
import { useMachine } from '@xstate/react';
​
export default function App() {
  const [current, send] = useMachine(jokeMachine)
  
  const { value, context: { joke }} = current
​
  const FetchNewJokeButton = () => (
    <TouchableHighlight style={styles.button} onPress={() => send('FETCH_JOKE')}> <Text style={styles.subtitle}>Fetch new joke</Text> </TouchableHighlight>
  )
​
  return (
    <View style={styles.container}> <Text style={styles.title}>Joke App!</Text> {value === 'idle' && <FetchNewJokeButton />} ​ {value === 'loading' && <ActivityIndicator />} ​ {value === 'failed' && <Text style={styles.subtitle}>An error occurred :(</Text>} {!!joke && value === 'loaded' && ( <> <Text style={styles.joke}>{joke}</Text> <FetchNewJokeButton /> </>
      )}
      <StatusBar style="auto" />
    </View>
  );
}

Let's now have a look at this code. We first import the machine and the useMachine hook.

import jokeMachine from './src/machines/jokeMachine'
import { useMachine } from '@xstate/react';

Then, we execute the useMachine hook, passing the machine as parameter. This hook returns the current object and a send function that sends an event to do the state transition.

The current object contains the actual state of the machine and its context.

We also created a new component that sends the event FETCH_JOKE when it's pressed.

  const [current, send] = useMachine(jokeMachine)
  
  const { value, context: { joke }} = current
  
  const FetchNewJokeButton = () => (
    <TouchableHighlight style={styles.button} onPress={() => send('FETCH_JOKE')}> <Text style={styles.subtitle}>Fetch new joke</Text> </TouchableHighlight>
  )

In the App.js jsx, we compare the states and show the appropriate component to the final user.

  return (
    <View style={styles.container}> <Text style={styles.title}>Joke App!</Text> {value === 'idle' && <FetchNewJokeButton />} ​ {value === 'loading' && <ActivityIndicator />} ​ {value === 'failed' && <Text style={styles.subtitle}>An error occurred :(</Text>} {value === 'loaded' && ( <> <Text style={styles.joke}>{joke}</Text> <FetchNewJokeButton /> </>
      )}
      <StatusBar style="auto" />
    </View>
  );

To Conclude

In this post, we described what a finite state machine is, how we can manage states, how context works, and how we can add actions when a state is triggered. I hope this post has helped you better understand what state is and how you can manage it more easily with XState.

P.S. Here's a nice tool if you want to visualize your state machine. Try to put our machine in this tool!

P.P.S. Here's the code repo for all the above code.

KEEP MOVING FORWARD

Lucas Dutra / code