DEV Community

Cover image for You're doing state wrong
Nabil Tharwat
Nabil Tharwat

Posted on • Originally published at nabiltharwat.com

You're doing state wrong

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!

Top comments (17)

Collapse
 
lexlohr profile image
Alex Lohr

State machines are a nice pattern, but true and false could also make up the entirety of states of a simple state machine. You don't need to make an enum of them.

Collapse
 
vampeyer profile image
Jake T. (Vampeyer )

Can you do a demo so I can refer to the code ?
Thank you sir -

Collapse
 
kl13nt profile image
Nabil Tharwat

I added a code example in the Exceptions section of the article. :)

Collapse
 
lexlohr profile image
Alex Lohr
const [on, setOn] = useState<Boolean>();
const toggle = () => setOn(o => !o);
Enter fullscreen mode Exit fullscreen mode

I see the author added a part "the exception" and I welcome the addition.

Collapse
 
aminnairi profile image
Amin

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).

class FormIdleState { }

class FormSubmittingState {
  public constructor(
    public readonly email: string,
    public readonly password: string
  ) { }
}

class FormErrorState {
  public constructor(public readonly error: Error) { }
}

class FormSubmittedState { }

type State 
  = FormIdleState 
  | FormSubmittingState 
  | FormErrorState
  | FormSubmittedState

function handleState(state: State) {
  if (state instanceof FormIdleState) {
    alert("Please, login in order to access this app");

    // @ts-expect-error Just for the demo, but this is not possible
    console.log(state.email);

    return;
  }

  if (state instanceof FormSubmittingState) {
    alert(`Loggin with email ${state.email}`);
    // TODO: send an HTTP request

    // @ts-expect-error Just for the demo, but this is not possible
    console.log(state.error.message);

    return;
  }

  if (state instanceof FormErrorState) {
    alert(`There has been an error: ${state.error.message}`);

    // @ts-expect-error Just for the demo, but this is not possible
    console.log(state.email);

    return;
  }

  alert("Successfully logged in");
  // TODO: redirect to profile page

  // @ts-expect-error Just for the demo, but this is not possible
  console.log(state.email);
}

// Change me!
const state: State = new FormSubmittingState(
  "email@domain.com",
  "p4ssw0rd"
);

handleState(state);
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
kl13nt profile image
Nabil Tharwat

Definitely one way to look at it. Sadly it's too verbose in TS.

Collapse
 
lucasayabe profile image
lucas-ayabe

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.

Collapse
 
thexdev profile image
M. Akbar Nugroho

Nice write! What's software you use to make that diagram?

Anyway, I also write article about simplifying complex state in React with reducer :)

Collapse
 
jankapunkt profile image
Jan Küster

I'd also like to know the tool to model the state machines.

Collapse
 
kl13nt profile image
Nabil Tharwat • Edited
Collapse
 
kl13nt profile image
Nabil Tharwat
Collapse
 
best_codes profile image
Best Codes

Very interesting, thanks for writing!

Collapse
 
lloydwright profile image
Lloyd wright

Great it's too help Full...

Collapse
 
yusuke050 profile image
Lin Fan-Chiang

I love the concept of a "state machine" for its simplicity and ease of understanding.

Collapse
 
syedmuhammadaliraza profile image
Syed Muhammad Ali Raza

Good one

Collapse
 
kl13nt profile image
Nabil Tharwat

Thank you 🙏

Collapse
 
vampeyer profile image
Jake T. (Vampeyer )

Thank you !

Some comments may only be visible to logged-in visitors. Sign in to view all comments.