Modelling using statecharts changed my career as a dev. Of all the state management solutions I’ve tried, it feels the most complete, logical and robust. Even if you don’t use them in your app’s code, statecharts let you break down complex features into states, events, services, actions and guards.
It took me a long time to get comfortable modelling with statecharts. Even when I’d learned all the terms, it took time to work out a step-by-step process for building statecharts from scratch.
Today, I’m going to share an opinionated, step-by-step guide for building statecharts from scratch. This process works for me, but it might not work for you. Feel free to tweak it as you go.
What you’ll need​
- A pen and paper, or a digital notepad of some kind
- A statechart builder, such as our visual editor or an XState machine in our XState visualizer
- A clear idea of what you’re building. Maybe something you’ve implemented at work? You could also pick something from XState Catalogue.
I’ve also built this process as a statechart using our visual editor.
1. List all the possible events​
The first step in this process is to work out all the events that can be received by your statechart. You can think of an event as ‘something that happens’ in your app. There are plenty of examples even on this page:
- Press the escape key
- Press the space bar
- Select some text
- Click on an image
You don’t need to list all possible events that the user can perform. You only need to list the events that your statechart cares about. Here are some examples:
For a submit form:
- User changes the value of an input
- User submits form
For a spreadsheet:
- User clicks a cell on the spreadsheet
- User holds down the
shift
/ctrl
key - User presses
escape
- User scrolls up or down
Seeing all the events in a big list may start giving you an idea of what is possible in your statechart. You might start thinking in terms of sequences of events — i.e. User changes input
-> User submits form
. Write down any sequences that pop into your head, they’ll be useful later.
2. List all the possible tasks​
Next, it’s important to consider the tasks your app needs to perform. These tasks could be called ‘side effects’ — things that happen as a result of your statechart running. These could be as diverse as:
- Adding an item to a todo list (in local state)
- Sending a request to the API to load some data
- Focusing an input
- Waiting for a video to load
- Subscribing to something for updates (perhaps via
window.addEventListener()
)
NOTE: I’m using ‘tasks’ loosely. This isn’t an official term in the XState docs — but ‘services’ and ‘actions’ are.
Once you have a list of tasks, you need to divide them into two groups.
2a. Services​
The first group is for services, tasks where you need to do something when they finish. I wrote a longer guide about the distinction between actions and services here.
From our list above, these are services:
- Sending a request to the API to load some data
We need to get something from the API, meaning that we need to wait until we receive the data. This task can also fail — if we’re having network trouble or the API method fails. That means we care whether it succeeds or fails.
- Waiting for a video to load
Same as above — we need to wait for the video to be loaded, and we care if it fails to load.
- Subscribing to something for updates
Here, it’s a little different — when you subscribe to something, you need to clean up the listener to prevent a memory leak. For instance:
const listener = () => {
console.log("Hello!");
};
// Subscribe
window.addEventListener("focus", listener);
// Unsubscribe
window.removeEventListener("focus", listener);
Here, we care about the outcome because we need to run something at the end of the process — i.e. unsubscribe from the listener.
Adding onDone/onError events​
Service completions/errors are handled as events in your statechart, meaning they’re on the same level as your user clicking buttons.
When you’ve got your list of services, note down two things:
For each service that we need to wait for it to complete, add a serviceName.onDone
event to your list.
For each service that might reasonably be expected to error, add a serviceName.onError
event to your list.
2b. Actions​
The second group is for actions, tasks that you can ‘fire and forget’. Unlike services, the statechart forgets about actions as soon as they’re fired.
From our list above, these are actions:
- Adding an item to a todo list (in local state)
Changes to local state are pretty much always fire-and-forget. The reason is that, since we manage the local state ourselves, updating it is instant. XState’s assign action is a good example.
- Focusing an input
Focusing an input, in the same vein, is fire-and-forget. We don’t care about the outcome, and it’s unlikely to fail.
3. Work out the very first state​
Now that you know what can happen (events) and what can be done (actions & services) in your statechart, it’s time to start adding some states.
3a. Know your statechart’s lifecycle​
It’s always easiest to start at the beginning. Before you add your first state, consider the moment that your statechart gets initiated. What causes your statechart to run? Some examples:
An authentication statechart, which manages the state for whether the user is logged in to a website or not. This would be started the first moment the user clicks on to any page of your app, and finished when they close your app.
A sign up form statechart, which handles a user signing up to your app. This might be started when the user visits the /sign-up
route, and stopped when they exit it.
3b. Write down your first state​
Now that you know what your app looks like when your statechart gets initiated, it’s time to name its initial state. Consider what the statechart is doing at that time. It could be Loading data
, or Waiting for user to submit form
, or even just Idle
, waiting for something to happen.
Dynamic initial states​
Every statechart must have an initial state, and it can’t be dynamic — it must be the same every time your statechart runs.
If you feel your statechart does have more than one initial state (for instance it could start in two different modes) consider using a ‘checking’ state via an eventless transition.
4. Build out the states​
Now that you have your first state, you can start the process of building out the states. Every state represents a length of time, so consider what is happening during that state.
4a. Work out if any tasks are running​
Do you have any services running? If so, invoke those services using XState’s invoke property.
Does an action need to happen when you enter or exit the state? If so, add it as an entry or exit action.
Remember, the statechart itself is also a state. We often call it the ‘root state’. This means that you can run services or listen to events for the entire duration of your statechart. You can also run entry actions when your statechart starts, and exit actions when it stops.
4b. Work out which events can happen in that state​
Consider the period of time your state represents. Which events should do something, and what should they do?
Events that change state​
If an event results in:
- A new service running
- Something new appearing on screen
- Other types of events becoming possible
- A current service stopping
Then it might need to move to a new state. A great example is a data fetcher. Your app is in two distinct states:
- Fetching data: it doesn’t yet have the data, and the ‘fetch data’ service is running.
- Showing data: it has the data, and is showing it on screen. The ‘fetch data’ service has stopped.
If you have an event like this, draw out the new event and either create a new state, or make it target an existing one if needed.
Events that don’t change state​
Sometimes, events can be used to fire an action instead of changing state. A good example of this is when a form input changes, and you need to save the new value to local state.
This is called a self-transition, where the event doesn’t change the state — the state transitions to itself.
Events that do nothing​
It’s important to bear in mind that when your statechart is in a certain state, only the events that you specify will be handled. In other words, any event you don’t specify will do nothing when it’s sent to the statechart.
A classic example of this is a form. When you submit the form, you go to the ‘submitting’ state. It’s important that you don’t allow the ‘submit’ event to be received while in the ‘submitting’ state — otherwise the form might get sent twice!
5. Keep going!​
Once you’ve figured out which actions/services are running in which states, and what all the events do, you’ve modelled your first state! You’ll likely have states which branch off your initial state — so go through those one-by-one and build them out.
You can also leave parts of your statechart unimplemented, and dive into building the frontend/actions/services before returning to modelling again.
I’ve found this approach really useful when getting to grips with what my app does. You can even use a statechart as an early validation tool to confirm that what you’re building is correct.
If you’ve got any more questions, do join our Discord and ask in the ‘modelling-help’ channel.