State Management: How to tell a bad boolean from a good boolean
Tags: webdev, tutorial
Published: 2021-05-27 14:05:02

**TL;DR**: Bad booleans represent state. Good booleans are derived from state.

When you're managing state in your app, it's easy to fall prey to bad booleans. Bad booleans look like this:

```ts
let isLoading = true;
let isComplete = false;
let hasErrored = false;
```

On the surface, this looks like good code. It appears as though you've represented three separate states with proper boolean names. In the 'model' you've pictured for your state, only one of these states can be true at any one time.

In a fetch request, you might model the state like this:

```ts
const makeFetch = async () => {
  isLoading = true;
  try {
    await fetch('/users');

    isComplete = true;
  } catch (e) {
    hasErrored = true;
  }
  isLoading = false;
};
```

Again, this looks nice. We're orchestrating our booleans as we move through the async request.

But there's a bug here. What happens if we make the fetch, it succeeds, and we make the fetch again? We'll end up with:

```ts
let isLoading = true;
let isComplete = true;
let hasErrored = false;
```

## Implicit states

You probably hadn't considered this when you made your initial model. You may have frontend components which are checking for `isComplete === true` or `isLoading === true`. You might end up with a loading spinner _and_ the previous data showing at the same time.

How is this possible? Well, you've created some implicit states. Let's imagine you considered 3 states as ones you actually wanted to handle:

1. `loading`: Loading the data
2. `complete`: Showing the data
3. `errored`: Erroring if the data doesn't turn up

Well, you've actually allowed _8_ states! That's 2 for the first boolean, times 2 for the second, times 2 for the third.

This is what's known as boolean explosion - I learned about this from [Kyle Shevlin's egghead course](https://egghead.io/lessons/javascript-eliminate-boolean-explosion-by-enumerating-states).

## Making states explicit

How do you get around this? Instead of a system with 8 possible values, we need a system with three possible values. We can do this in Typescript with an enum.

```ts
type Status = 'loading' | 'complete' | 'errored';

let status: Status = 'loading';
```

We'd implement this in a fetch like this:

```ts
const makeFetch = async () => {
  status = 'loading';
  try {
    await fetch('/users');

    status = 'complete';
  } catch (e) {
    status = 'errored';
  }
};
```

It's now impossible to be in the 'loading' and 'complete' state at once - we've fixed our bug. We've turned our bad booleans into a good enum.

## Making good booleans

But not all booleans are bad. Many popular libraries, such as `react-query`, `apollo` and `urql` use booleans in their state. An example implementation:

```tsx
const [result] = useQuery();

if (result.isLoading) {
  return <div>Loading...</div>;
}
```

The reason these are good booleans is that their underlying mechanism is based on an enum. Bad booleans _represent_ state. Good booleans are _derived_ from state:

```ts
let status: Status = 'loading';

// Derived from the status above
let isLoading = status === 'loading';
```

You can safely use this `isLoading` to display your loading spinner, happy in the knowledge that you've removed all impossible states.

## Addendum: Enums in Javascript

A couple of folks in the comments are asking how we represent a state enum in Javascript. While the above code will work without typings, you can also represent enums as an object type.

```js
const statusEnum = {
  loading: 'loading',
  complete: 'complete',
  errored: 'errored',
};

let status = statusEnum.loading;

const makeFetch = async () => {
  status = statusEnum.loading;
  try {
    await fetch('/users');

    status = statusEnum.complete;
  } catch (e) {
    status = statusEnum.errored;
  }
};
```