You're doing state wrong

Nabil Tharwat - Jun 9 - - Dev Community

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Media player state machine

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
}
Enter fullscreen mode Exit fullscreen mode

Now state updates can be a single, consistent instruction:

setState('stopped')
// or
setState(State.STOPPED)
Enter fullscreen mode Exit fullscreen mode

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!

Form state machine

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)
        })
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . .