Implementing component state as a combination of booleans may seem like the easiest way to do it, but let's do something different.
Cover by Namroud Gorguis on Unsplash
This article is framework- and language- agnostic. Code examples presented are written in a generic form.
Consider a music player
That can play, pause, and stop. Developers are often tempted to represent
each state in a separate boolean:
const isStopped = createState(true)
const isPlaying = createState(false)
const isPaused = createState(false)
If you think about this for a moment, each of those boolean states can be either true or false. Counting all possibilities yields 8 possible state variations, when our component only has 3 actual states. Which means we have 5 impossible states in our tiny component.
Impossible states are states that the component is never meant to be in, usually indicating a logic error. The music player can't be playing and stopped at the same time. It also can't be paused and playing at the same time. And so on.
Guard statements usually accompany boolean states for this reason:
if (isStopped && !isPlaying && !isPaused) {
// display stopped UI
} else if (!isStopped && isPlaying && !isPaused) {
// display playing UI
} else if (!isStopped && !isPlaying && isPaused) {
// display paused UI
}
And state updates turn into a repetitive set of instructions:
// To play
setIsPlaying(true)
setIsPaused(false)
setIsStopped(false)
// To stop
setIsPlaying(false)
setIsPaused(false)
setIsStopped(true)
Each addition and modification later to the component needs to respect these 3 valid states, and to guard against those 5 impossible states.
Hello, state machines!
Every program can be simplified into a state machine. A state machine is a mathematical model of computation, an abstract machine that can be in exactly one of a finite number of states at any given time.
It has a list of transitions between its defined states, and may execute effects as a result of a transition.
If we convert our media player states into a state machine we end up with a machine containing exactly 3 states (stopped, playing, and paused), and 5 transitions.
Now we can represent our simple machine in a single state that can be anything, from a Union Type to an Enum:
type State = 'stopped' | 'playing' | 'paused'
enum State {
STOPPED,
PLAYING,
PAUSED
}
Now state updates can be a single, consistent instruction:
setState('stopped')
// or
setState(State.STOPPED)
With this approach we completely eliminate impossible states, make our state easier to control, and improve the component's readability.
What about effects?
An effect is anything secondary to the component's functionality, like loading the track, submitting a form's data, etc. An action.
Let's consider forms. A form is usually found in one of four states: idle, submitting, success, and error. If we use boolean states we end up with 4 booleans, 16 possible combinations, and 12 impossible states.
Instead, let's make it a state machine too!
The code behind this machine can be as simple as another method on the component:
enum State {
IDLE /* default state */,
SUBMITTING,
ERROR,
SUCCESS
}
const submit = (formData: FormData) => {
setState(State.SUBMITTING)
postFormUtility(formData)
.then(() => {
setState(State.SUCCESS)
})
.catch(() => {
setState(State.ERROR)
})
}
The exception
Obviously there are cases where a component may truly have only 2 states, therefore using a boolean for it works perfectly. Examples of this are modals to control their visibility, buttons to indicate a11y activation, etc.
const isVisible = createState<boolean>(false)
const toggle = () => {
setState(!isVisible)
}
The problem starts to form when you introduce multiple booleans to represent variations of the state.
I still need booleans!
You can derive booleans from your state. Control your component through a single state machine variable, but derive a hundred booleans from it if you want.
Using the form example:
enum State {
IDLE /* default state */,
SUBMITTING,
ERROR,
SUCCESS
}
const state = createState(State.IDLE)
const isSubmitting = state === State.SUBMITTING
const hasError = state === State.ERROR
const isSuccessful = state === State.SUCCESS
Wrap up
Thinking of components as state machines has helped me simplify a lot of codebases. It's effect on the overall accessibility of a codebase is truly immense. Try it and tell me what you think! 👀
Thanks for reading! You can follow me on Twitter, or read more of my content on my blog!
Top comments (17)
State machines are a nice pattern, but
true
andfalse
could also make up the entirety of states of a simple state machine. You don't need to make an enum of them.Can you do a demo so I can refer to the code ?
Thank you sir -
I added a code example in the Exceptions section of the article. :)
I see the author added a part "the exception" and I welcome the addition.
Although there is no possibility to create an algebraic data type in TypeScript, we could artificially simulate it using a combination of classes and type discrimination in order to get a state that is both predictable, warning impossible states and the possibility to embed state in our "types" (that are really classes in a union).
Note that this example uses TypeScript in order to prevent most human mistakes.
In my opinion, having ADT in TypeScript would remove the need for state machines.
Definitely one way to look at it. Sadly it's too verbose in TS.
at this point, you should consider using OOP instead of ADTs to solve this, like move the ifs to the respective classes as methods in order to implement something like the State pattern, making the code less verbose.
Nice write! What's software you use to make that diagram?
Anyway, I also write article about simplifying complex state in React with reducer :)
Simplify state with reducer
M. Akbar Nugroho ・ Mar 8 '23
I'd also like to know the tool to model the state machines.
stately.ai
stately.ai
Very interesting, thanks for writing!
Great it's too help Full Pentagon Detailing...
I love the concept of a "state machine" for its simplicity and ease of understanding.
Thank you !
Good one
Thank you 🙏
Some comments may only be visible to logged-in visitors. Sign in to view all comments.