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;
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;
};
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;
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:
loading
: Loading the datacomplete
: Showing the dataerrored
: 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";
We’d implement this in a fetch like this:
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:
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:
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​
We can represent a state enum in Javascript as well. While the above code will work without typings, you can 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;
}
};