Improving multi-step forms with state machines
Press Start
Its the first day at your ✨ brand new ✨ frontend job. Bright eyed and bushy tailed you pluck a task from the onboarding list: “add a new step to an existing multi-step form”. Sounds simple enough.
Turns out the form is business critical, handles almost all end-user input, doesn’t restrict user navigation between it’s many form steps and is tricky to follow as the flow relies heavily on a decentralised scattering of component-driven conditionals — the kind of service built under time pressures.
How do users actually flow through this form and what exactly are the edge cases? It’s not clear from the code and you realllly don’t want to introduce a bug.
You grope for documentation but struggle to find much beside a few sparse notes which are limited in scope and almost certainly out of date.
You load up the application locally, attempting to manually work through the branching flows yourself but it’s time consuming and you cant quite shake the feeling you missed a few.
You ask around for technical domain experts only to find they’ve all moved on.
You feel a bit like a level 1 adventurer, thrust into a high level dungeon without a map — a dungeon replete with trap doors, snake-pits and dragons 🐲
You cast your eyes to the heavens — ‘there must be a better way!’
A second chance
It’s a bit of an exaggeration but I felt a little like that level 1 adventurer when I first started at First AML; in my case the multi-step form in question is our EIV (electronic identity verification) service, and it handles the capturing of critical input from end users around the world. A very important but, in true start up fashion, swiftly built service.
As luck would have it, not long after starting, I was tasked with the frontend tech design of a greenfield rebuild of this service and I knew from the get go that this form was destined to grow rapidly both in terms of complexity and importance.
My first point of action was to think back to my time as a First AML noob; what were the main pain points?
Difficulty preventing unexpected user flows or related edge-case errors
Difficulty finding, remembering and documenting the various user flows and edge cases
Difficulty communicating new flow changes across product, design, QA and engineering
Difficulty scaling the form flow
Documentation might alleviate some of these pains buuuuut I don’t really want to write up and maintain endless documentation for a continuously changing service.
That, and documentation doesn’t really help us lock down form flows or prevent unexpected runtime user errors.
How can we tame that complexity and simplify iteration while still having solid documentation?
Enter ✨ finite state machines ✨
State machines are an old, well-loved and ubiquitous design pattern. The more you look the more you see how they quietly power much of the world we live in — though they only really made their way to the frontend web development space in recent years with the rise of XState.
So what actually are state machines? Essentially a state machine is a system which helps us define and enforce a flow chart. In other words, they allow us to impose watertight restrictions on transitions from one finite state to another.
An example speaks a thousand words here:
In the above diagram, all possible states that our state machine can be in are represented by colours (red
, green
and amber
), all possible transitions between these states are represented by the arrows and these transitions are triggered by events (the timers expiring).
The beauty of this simple concept (and in fact this is the core of state machines) is that the green light can never transition to a red light! It’s not possible with state machines. Whew… what a load off.
In our EIV form, we seriously don’t want users to give us their PII (personally identifiable information) without first explicitly consenting to our terms and conditions! This is just one example of where, in web development, a slip up could cause a real mess — just like traffic lights suddenly going from green
to red
.
Awesome, so this state machine thing can help us prevent unexpected user flows by restricting transitions between states (i.e. transitions between form steps)! So far so good.
Now how does this thing help us with finding, remembering and documenting the various user flows and edge cases?
Documentation is dead, long live documentation!
Turns out state machines are super easy to visualise as flow charts — in fact they basically are the living incarnation of flow charts!
XState takes full advantage of this and provides a super neat visualiser which takes in our coded-up state machine and spits out a human readable flow chart:
And just like that — living documentation 🎉 A new, common, visual, interactive language for technical and non technical people alike. This should help with finding, remembering and documenting flows!
What’s more, it’s as simple as pie to share — you just copy pasta the visualisation link and bob’s your uncle… you’re able to share an up to date, interactive visualisation.
Need to add that new step into your multi step form? Simple. Just make your state-machine code changes, copy paste them to your visualisation on Stately.ai and grab a screen shot or share the link. This pairs especially well with a brief, high level description of what you’ve done and why 🍷 Delicious.
This feature alone has improved my ability to communicate new flow changes across product, design, QA and engineering. Although its mostly self-explanatory, an introduction to interpreting the visualisation can pay dividends in the long run.
Scale with confidence 🚀
Aight, so we’ve talked about how state machines can help:
Prevent unexpected user flows or related edge-case errors
Document the various user flows and edge cases
And communicate new flow changes across product, design, QA and engineering
But what about scaling? First AML is a rocket ship scaling globally! The form needs to keep up.
For me, this sort of intense scaling really comes down to two key questions:
Can you make software changes with confidence?
Can you handle exponential cyclomatic complexity elegantly?
XState is not perfect, but it sure as hell helps in both of these cases.
Let’s say, theoretically, that I got my dream job as head of Playing Video Games All Day at Twitch. And, oh no, that happens to coincide with First AML hiring a ton of level 1 adventurers! Normally I’d spend my final days blearily writing out documentation, holding upskilling sessions and fever-dreaming in flow charts.
With XState, as the documentation is alive, visual and interactive it’s relatively easy to follow regardless of the level of the adventurer. The adventurer now has a map! I just post the link and field follow up questions 😎 Much more time to daydream about the new role.
What’s more, because the form flow itself is abstracted away from the component level and into a state machine exoskeleton, as long as the level 1 adventurer is not making changes to the state machine itself the chance of them introducing an unwanted flow change or unexpected edge-case is significantly reduced. They’d have to go out of their way to let an end user submit PII without providing consent!
In terms of handling cyclomatic complexity (i.e. the number and depth of our user flows), this is what state machines were built for! Hell, only a few months after rebuilding the EIV service, check out how much complexity we’ve had to add:
Now imagine doing that without a state machine having your back 😟 💦
In practice
Now that you’re a convert 👏 let’s take a final look at how this sort of thing shakes out in practice — i.e. in frontend code land.
Let’s start by breaking down a simplified XState representation of our EIV form. The situation is:
We want three finite states each representing a step in the multi step form.
We want the ability to store our end user consent and end user details.
We want to ensure that only the users who have explicitly provided consent are able to give us their PII.
We want to allow users to move from step one to step two to step three in a linear fashion. On the last step, the users should only be able go
back
.
This is the name of our state machine!
This is the initial finite state our machine will load in.
This is the non-finite state store which holds arbitrary chunks of data (perfect for things like user data! You can think of this as your redux state store). Here we have set some sane defaults.
Here we list out each of our three finite states!
These key value pairs tell each state which events to look out for and how to behave in each of those cases. The
target
is the state to transition to and thecond
is the guard in place to gate this transition. NOTE: any other event will be ignored.Here we register all our possible actions. In other words, this is a list of behaviours (you can think of these as event callbacks) which can be used in any of the above finite state configuration objects.
Finally we have guards! These nifty functions (which simply return
true
orfalse
) help states know when they can or cannot transition.
There is a bit of an XState learning curve — but personally I didn’t find it too bad. And the interactive visualiser seriously helps with this.
PS: You can have a play with this state machine here.
. . .
Nice! Ok… but, like, how does this work with something like React? What’s the cheese with routing? Show me some cooooode!
Once you have your state machine it’s simple as pie:
✨ Presto! ✨ You don’t need to worry about flow-related logic inside scattered across components. You don’t need to worry if the component should be able to go back or not. You don’t need to break your back documenting the intricacies of this flow! And because your state machine’s context holds the user data, hydrating that form when the user goes back
is super easy too!
Most importantly, you don’t need to worry if a level 1 adventurer working inside of Consent
accidentally calls next
before the user has consented… our state machine won’t have any of it.
. . .
I’ve only really scratched the surface of XState and state machines in UI development but hopefully you’ve got a better idea of what they are and when they can make your life easier!
Next step: read every line of XState docs twice.