Use a status enum instead of booleans

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 2 years ago
Updated 2 years ago

Almost every UI app I've seen has an isLoading boolean somewhere in the code. And if it's not that, it's some variation of a boolean that's intended to represent some state. Unfortunately, very often this leads to states that exist in the real world that are unrepresentable in the code, or states in the code that are impossible in the real world. In either case, you've got yourself a bug.

From this lesson you'll learn how to avoid this by using a status enum instead of an isLoading boolean in a React custom hook for retrieving the user's position via the geolocation API.

This is based on my blog post: Stop using isLoading booleans.

Kent Dodds: [0:00] Here, we have a little app that's showing my latitude and longitude. I have my DevTools open with the sensors open so I can override the geolocation to specify my latitude and longitude to any of these major cities or specify something very specific if I want to.

[0:18] The way this is implemented is, I have this, YourPosition function component that's using a useGeoPosition custom hook. When we're loading, then we render loading your position. If we have a position, then we render that out. If we have an error, then we render that out.

[0:35] The way that this is implemented is, up here we have this useGeoPosition custom hook that uses a reducer to manage a isLoading position and error state. Then in our useEffect, we run that side effect to get the geolocation if it's available.

[0:51] Otherwise, we're going to just render out an error indicating that geolocation is not supported by this browser. If it is supported by the browser, then we'll watch the position. As the position changes, we'll dispatch an update to our state so that we can set the position to the current position.

[1:07] If there's ever an error like the user goes into a tunnel or something, then we'll dispatch the error so we can display that error state. When this component unmounts we're going to clear that watch, so we avoid memory leaks.

[1:18] Everything seems to check out here, but there's actually a bug. The first time that this renders, we're going to come in here and we'll have an isLoading is true because it's initialized to true in our initial state here. However, position's initialized to null and our error initialize to null, but that doesn't matter because we simply render out loading your position.

[1:36] When we get into this useEffect, we're going to skip over this if statement because we do support geolocation in this browser. We're going to set up our watcher here. When we get the user's location, we're going to dispatch a success that will trigger a rerender setting our position value, thanks to our reducer here. It also sets our loading state to false.

[1:58] As we go through and rerender this YourPosition, our isLoading state is now false. Our position is truthy and so we go into this if block. That will work as we're changing our position, we get that latitude and longitude updated perfectly. The problem comes in if we have an error.

[2:18] If I go ahead and say location unavailable, you'll notice that the latitude and longitude is still being displayed for the last location that we did have available. It's not showing us the error message that we want.

[2:30] This should make sense if we follow through this code. We're going to get an error, we're going to dispatch a type of error, we'll pass that error. In our reducer, we'll return all that same state, we'll set isLoading to false and we'll set the error to that error value that we get from the action. That triggers a rerender.

[2:47] Down here and your position, we're going to get our error and the position state is also still available and loading is false. We jump over this if statement because loading is false, but our previous positions still exists in state. That's why we're rendering that previous position, rather than rendering the error.

[3:05] Now, there are a couple of solutions to this. One solution might be that we should just require people who use this custom hook useGeoPosition to render not only the position, but also the error if there is one.

[3:17] Maybe, the users of this hook don't want to display this position or use that position if there was an error in retrieving the position or maybe they're just not thinking about it. A good API design requires that we make it easy to do the right thing and hard to do the wrong thing.

[3:31] Another solution to this could be to make sure that we clear out the position when we get an error. That would make it difficult for people who do want to show the most recent position, even when there has been an error.

[3:42] The last and best solution to this is to use a status rather than an isLoading Boolean. Let's take a look at what that would be like.

[3:51] Here, instead of isLoading, I'm going to say status and we'll initialize that to idle. Then, instead of setting isLoading on each of these, we're going to set the status. Instead of false, this will be a string. For the error case, we'll call this rejected. For the success case, we'll call this resolved.

[4:10] Then, we can come down here and instead of isLoading, we'll get the status. We can say if the status is idle, then we'll say we're loading your position. Otherwise, if the status is resolved, then we can render this out. Otherwise if the status is rejected, then we'll render out the error message here.

[4:34] With those changes, let's go ahead and refresh this. We'll see. Oh no, there was a problem getting your position, location unavailable. If our location becomes available, then we'll get a rerender showing our location. If we switch it back to unavailable, we'll get that our location is unavailable.

[4:51] This enables not only our use case where we're only showing one of these at a time, but it also enables the use case where we want to show the error state if there is an error state and the most recent position. Using a status instead of an isLoading Boolean gives us a lot more insight into what's going on with the state of this particular hook.

[5:09] To take things a step further, we could also have a switch case for started. Here, we'll return all the state and set the status to pending. Then we can come down here and dispatch a type of started. Down in here, we'll be able to say if the status is pending, then we'll return a div retrieving your position.

[5:39] Now, that changeover happened so quickly that it doesn't make a whole lot of sense to separate these two in our UI. Certainly, in other situations where you're asynchronously loading something and the status could sit at idle for an extended period of time, having that additional information can be quite useful.

[5:55] If you're not a huge fan of these equal signs everywhere, then you can make your own isLoading variable. We'll copy all that, say isLoading, put that there and we could have also an isResolved is status resolved and isRejected would be status rejected. Then we can come down here and say isResolved and isRejected.

[6:26] If you want to provide this kind of API for your users, then you can go ahead and do that as well. We'll come up here, we'll take our state and we'll just spread that state and derive those values from the state that we have in this particular hook.

[6:39] We can say state.status is idle or state.status is pending. I'll just make this a regular JavaScript object here. We could come down here and destructure isLoading, isRejected and isResolved here. Maybe that gives you the API that you're looking for without the drawbacks of not providing all of the information to the people who are using your code.

[7:05] With this, as we make changes to our position, that position will get updated. If we set the location unavailable, then that will get updated. We are bug free because we're giving all the information that we need.

[7:16] In review, all that we did here was we changed our isLoading Boolean to a status. We updated that status in our reducer here and returned that status. We also returned a couple convenience Booleans that are all derived from that status and users still have access to that status variable if they want to or they can use these Booleans.

[7:37] In any case, we're enabling users of our custom hook to have all the information they need for a bug-free experience.