DEV Community

loading...
Cover image for State Management: How to tell a bad boolean from a good boolean

State Management: How to tell a bad boolean from a good boolean

Matt Pocock
Lead developer at Stately, React, XState and Typescript lover, ex-voice coach.
Updated on ・3 min read

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:

let isLoading = true;
let isComplete = false;
let hasErrored = false;
Enter fullscreen mode Exit fullscreen mode

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:

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

    isComplete = true;
  } catch (e) {
    hasErrored = true;
  }
  isLoading = false;
};
Enter fullscreen mode Exit fullscreen mode

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:

let isLoading = true;
let isComplete = true;
let hasErrored = false;
Enter fullscreen mode Exit fullscreen mode

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.

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.

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

let status: Status = 'loading';
Enter fullscreen mode Exit fullscreen mode

We'd implement this in a fetch like this:

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

    status = 'complete';
  } catch (e) {
    status = 'errored';
  }
};
Enter fullscreen mode Exit fullscreen mode

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:

const [result] = useQuery();

if (result.isLoading) {
  return <div>Loading...</div>;
}
Enter fullscreen mode Exit fullscreen mode

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:

let status: Status = 'loading';

// Derived from the status above
let isLoading = status === 'loading';
Enter fullscreen mode Exit fullscreen mode

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.

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

Discussion (26)

Collapse
ultrox profile image
Marko Vujanic • Edited

You are missing, idle or 'not-asked' state here.

Recently I got interested into Elm and what I really like about that community is that they promote data structure first approach for building apps.

For curious, have a look more in depth about ideas borrowed from Elm here
lillo.dev/articles/slaying-a-ui-an...

Collapse
mpocock1 profile image
Matt Pocock Author

I'm assuming the data fetch is made when you first enter the page, meaning that the 'idle' state will likely only show for one render - so is not worth handling.

Love the look of elm but never tried it!

Collapse
lucas_castro profile image
Lucas Castro

Also, a common pattern that makes "idle" unnecessary on auto-loading states, is to start with the status as "loading" by default/definition.

Collapse
ahyagoub40 profile image
Ahmed Yagoub

Interesting article. Would love to see how the enum is defined and used in JavaScript too : )

Collapse
mdledoux profile image
Martin Ledoux

Ahmed,
This looks like a good and simple approach to achieving Enum in JS:

tutorialspoint.com/what-is-the-syn...

Note

Be sure to use Object.freeze(), as objects (and Arrays) can still have their properties/elements modified, even when declared as const.

Observation

Even in the author's post using TypeScript, Matt technically did not use an Enum construct, but rather he simulated Enum by defining a new Type. Enum is actually supported in TypeScript: typescriptlang.org/docs/handbook/e...

I'm not sure why he did this instead of defining an actual Enum, but it certainly seems to achieve the same end result. With that said, it sounds like you probably do not use TypeScript, so this observation is probably not very relevant for you.

Collapse
mpocock1 profile image
Matt Pocock Author

Hello! I find having strings defined like this makes for a better TS experience and easier debugging. It also makes the article easier to read for non-TS users.

Thread Thread
randomuser profile image
polyrytm • Edited

I get what you are saying, and i always opt for the way you did it here, but they are not enums. The approach is the same, but I don't really see why you would want to name them incorrectly. If readers want to find out more and search for 'typescript enums' they will see different things than you show here.

You are using a union of string literals.
typescriptlang.org/docs/handbook/2...

Thread Thread
mpocock1 profile image
Matt Pocock Author • Edited

Even though I haven't used the Typescript 'enum' constructor, what I've shown above is an enumerated set of values - an enum. I've used the above syntax because it's more common to declare enumerated events/messages/actions using union types than TS enums.

Thread Thread
randomuser profile image
polyrytm

Saying you're using a feature in typescript called enums will have interested people searching for 'typescript enums'. They will end up on a long 'typescript enums' page in the TS manual which is about 'typescript enums'... and none of it is about what you use here.

But more confusement is better i guess :)

Thread Thread
mpocock1 profile image
Matt Pocock Author

For those reading this thread, TS enums can achieve exactly the same behaviour as I described above.

Collapse
ahyagoub40 profile image
Ahmed Yagoub

Yeah, I don’t use TS and haven’t learned yet. Thanks for this

Collapse
mpocock1 profile image
Matt Pocock Author

Added as an addendum :)

Collapse
mongopark profile image
Ola' John Ajiboye

While this is good, it's not possible in Vanilla JS where you can't use enums or types. A simple idiomatic way to do this is to append .finally{isLoading = false} to the async block.

Collapse
mpocock1 profile image
Matt Pocock Author

Added as an addendum.

The code you describe will still result in the bug described in the article. The most sensible way to do this (resulting in the most predictable program with the fewest LOC) is with an enum.

Collapse
damms005 profile image
Damilola Emmanuel Olowookere

"Good boolean" practise is clearly the way to go. But curiously, is setting isLoading=false in .finally() not guarantee that there can't be a loading and complete state at the same time, which is what the described bug is about?

Thread Thread
mpocock1 profile image
Matt Pocock Author

Sure, you can of course fix the bug by adding more lines of code. But why choose an approach which forces you write defensive code to prevent impossible states?

Thread Thread
damms005 profile image
Damilola Emmanuel Olowookere

It seems you do not get my point. The case you made with good booleans, as I said, is the way to go. I am not arguing that. I am only trying to understand the statement you made with The code you describe will still result in the bug described in the article. Assuming good boolean is not to be used for whatever reason, according to that statement, setting isLoading=false in .finally() will still cause bug. I do not think so

Thread Thread
mpocock1 profile image
Matt Pocock Author

No - your approach would cause a different bug - you'd need to set isComplete to false at the start to fully ensure that the states were not mutual. As well as doing a similar thing for the errors. I think we both agree on the main point of the article.

Collapse
omril321 profile image
Omri Lavi

Thanks for the post! It reminded me of the talk Crafting Stateful Styles with State Machines by David Khourshid

Collapse
mahamoti1129 profile image
Mahamoti1129

Just so you know, there's a reddit user reposting this article as his own.

reddit.com/r/softwaredevelopment/c...

Collapse
mpocock1 profile image
Matt Pocock Author

Thanks for the heads up

Collapse
diogocorrea profile image
Diogo Corrêa

Nice post! I aways used booleans the "bad way". Gonna remember to make them derive from state next time ;)

Collapse
mrjjwright profile image
John Wright • Edited

Great article! This is what I also learned from Michel Westrate from Mobx, who teaches : "minimal state, derive, derive, derive!".

Collapse
waugh profile image
waugh

Before publishing, go on a "which" hunt. If there isn't a comma in front of "which", substitute "that". Unless you meant what comma followed by "which" means, in which case, put that.

Collapse
unfor19 profile image
Meir Gabay

Great article, thanks for the info, I'll definitely try to adopt this concept

Collapse
guivern profile image
Guillermo Verón

It makes perfect sense and even has made me think that representing the same (state) with different variables is not a good abstraction.
Great article! Thanks for sharing it.