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:
- An initial state
- A finite number of states
- A finite number of events
- A transition function to determine the next state for the current state and event
- 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 machineinitial
: the initial state of the machinestates
: the states of the machine, in this casewalking
andstopped
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 inonError
. - The fetch succeeds, updates the
joke
key inside the context with theassign
function, and the machine transitions to theloaded
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.