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 data -
complete
: Showing the data -
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';
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
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;
}
};
Top comments (27)
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...
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!
Also, a common pattern that makes "idle" unnecessary on auto-loading states, is to start with the status as "loading" by default/definition.
Interesting article. Would love to see how the enum is defined and used in JavaScript too : )
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.
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.
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...
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.
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 :)
For those reading this thread, TS enums can achieve exactly the same behaviour as I described above.
Yeah, I don’t use TS and haven’t learned yet. Thanks for this
Added as an addendum :)
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.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.
"Good boolean" practise is clearly the way to go. But curiously, is setting
isLoading=false
in.finally()
not guarantee that there can't be aloading
andcomplete
state at the same time, which is what the described bug is about?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?
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, settingisLoading=false
in.finally()
will still cause bug. I do not think soNo - your approach would cause a different bug - you'd need to set
isComplete
tofalse
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.Thanks for the post! It reminded me of the talk Crafting Stateful Styles with State Machines by David Khourshid
I used to use
pending
,fulfilled
andrejected
Just so you know, there's a reddit user reposting this article as his own.
reddit.com/r/softwaredevelopment/c...
Thanks for the heads up
Great article, thanks for the info, I'll definitely try to adopt this concept
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.
Nice post! I aways used booleans the "bad way". Gonna remember to make them derive from state next time ;)
Great article! This is what I also learned from Michel Westrate from Mobx, who teaches : "minimal state, derive, derive, derive!".