loading...
Cover image for Opinionated React - Use Status Enums Instead of Booleans

Opinionated React - Use Status Enums Instead of Booleans

farazamiruddin profile image faraz ahmad ・3 min read

Intro

I’ve been working with React for over four years. During this time, I’ve formed some opinions on how I think applications should be. This is part 6 in the series.

Why

When I started writing React, I would often use an isLoading boolean to indicate that I was loading some data asynchronously.

This is fine for a simple example, but as I learned it does not scale well.

Why It's a Bad Idea - an Example

import * as React from "react";
import { getUserById } from "./services/user-service";
import { User } from "./types/user";

export function App() {
  const [user, setUser] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(true);

  React.useEffect(() => {
    const handleGetUser = async (id: string) => {
      const user = await getUserById(id);
      setUser(user);
      setIsLoading(false);
    };

    handleGetUser("1");
  }, []);

  return (
    <div>
      {isLoading && <p>Loading...</p>}
      {!isLoading && user && <UserProfile user={user} />}
    </div>
  );
}

function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <p>{user.displayName}</p>
      <p>{user.email}</p>
    </div>
  );
}

Here's an example where we are fetching a user and flip a boolean value to indicate that we are done loading. This is fine...but we don't really know if our handleGetUser function successfully fetched the user.

user could still be null if the fetch call failed.

We could add a try / catch block to our handleGetUser function, like so.

import * as React from "react";
import { getUserById } from "./services/user-service";
import { User } from "./types/user";

export function App() {
  const [user, setUser] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(true);
  const [errorMessage, setErrorMessage] = React.useState('');

  React.useEffect(() => {
    const handleGetUser = async (id: string) => {
      try {
        // Clearing the error message.
        setErrorMessage('');
        const user = await getUserById(id);
        setUser(user);
      } catch (error) {
        setErrorMessage(error.message)
      }
      // Set isLoading to false regardless of 
      // if the call succeeds or fails.
      setIsLoading(false);
    };
    handleGetUser("1");
  }, []);

  return (
    <div>
      {isLoading && <p>Loading...</p>}
      {!isLoading && user && <UserProfile user={user} />}
    </div>
  );
}

function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <p>{user.displayName}</p>
      <p>{user.email}</p>
    </div>
  );
}

We're now tracking error messages, but we still didn't really solve our problem of knowing what happened after isLoading is set to false. We have to do some checks to figure it out.

// loading
isLoading === true

// success
isLoading === false && user !== null && !error

// error
isLoading === false && !user && error !== ''

Even with a few different statuses, we have to do too much thinking.

A Better Approach - use Enums

An enum (short for enumeration), allows us to define a set of named constants. These constants can be used to create a set of distinct cases.

export enum UserStatus {
  LOADING = "loading",
  SUCCESS = "success",
  ERROR = "error",
}

We can define our distinct "states", and use them like so:

*Note that I'm using three separate useState calls here, not something I would actually do. This is for the purpose of learning. If you want to learn how I manage state, you can check out this post.

import * as React from "react";
import { getUserById } from "./services/user-service";
import { User } from "./types/user";
import { UserStatus } from "./constants/user-status";

export function App() {
  const [user, setUser] = React.useState<User | null>(null);
  const [status, setStatus] = React.useState<UserStatus>(UserStatus.LOADING);
  const [errorMessage, setErrorMessage] = React.useState<string>('');

  React.useEffect(() => {
    const handleGetUser = async (id: string) => {
      try {
        // Clearing the error message.
        setErrorMessage('');
        const user = await getUserById(id);
        setUser(user);
        setStatus(UserStatus.SUCCESS);
      } catch (error) {
        setErrorMessage(error.message)
        setStatus(UserStatus.ERROR);
      }
    };
    handleGetUser("1");
  }, []);

  if (status === UserStatus.ERROR) {
    return <div><p>Oops, something went wrong.</p></div>
  }

  return (
    <div>
      {status === UserStatus.LOADING && <p>Loading...</p>}
      {status === UserStatus.SUCCESS && <UserProfile user={user} />}
    </div>
  );
}

function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <p>{user.displayName}</p>
      <p>{user.email}</p>
    </div>
  );
}

This is a lot easier to reason about, and allows us to add some more states later if needed. 👍

Wrapping Up

This is the 6th post in my Opinionated React series. As always, I'm open to feedback.

If you'd like more content like this, or have some questions, you can find me on Twitter.

Till next time.

Discussion

pic
Editor guide
Collapse
mxldevs profile image
MxL Devs

The first time I ran into this problem, I switched to using numbers. 0 = not started, 1 = in progress, 2 = ready, 3 = post loading processes, etc.

But then everytime I went back to look at my code a week later I forgot what the numbers meant and it was just a huge pain. Then I found out about enums.

Collapse
farazamiruddin profile image
faraz ahmad Author

I did the same! But like you said, having to remember what all the numbers represented was difficult.

Collapse
mxldevs profile image
MxL Devs

I actually tried using constants. NOT_STARTED, LOADING, etc. But enums was still better because of auto-complete and working with interfaces. Taking an enum type as the status is just more descriptive than an int, whether it's written as a constant or otherwise lol

Collapse
clamstew profile image
Clay Stewart

I’ve done this a dozen times. I think you’re right to formalize it. Nice post.

But I tend to agree with some of the comments. One great thing about selectors in redux is the ability to easily derive state. And I think there is still a space for functions that derive react hooks state based on state like data and loading without having to store additional state. This is one reason I’ve been leaning on useReducer, because you can export selectors from the hook, too, since it has access to a whole sub-state object.

Collapse
farazamiruddin profile image
faraz ahmad Author

Thank you 🙏🏽

Collapse
guettli profile image
Thomas Güttler

Yes, that's true.

It's true for databases too.

If you want to store a data in a SQL database which has three states (True, False, Unknown), then you might think a nullable boolean column (here "my_column") is the right choice. But I think it is not. Do you think the SQL statement "select * from my_table where my_column = %s" works? No, it won't work since "select * from my_table where my_column = NULL" will never ever return a single line. If you don't believe me, read: Effect of NULL in WHERE clauses (Wikipedia). If you like typing, you can work-around this in your application, but I prefer straight forward solutions with only few conditions.

If you want to store True, False, Unknown: Use text, integer or a new table and a foreign key.

From my Guidelines: github.com/guettli/programming-gui...

Collapse
farazamiruddin profile image
faraz ahmad Author

Great comparison! Thank you for sharing this!

Collapse
nosknut profile image
nosknut

I disagree with this article. I dont think you should be using a loading state at all. If the data is null, you are loading. If an error is present, the loading failed. Using a state with an enum adds complexity because now the same information is relayed through 3 different sources; the error state containing the error message/response, the value or resource state that contains the actual data on success, and your handcrafted enum/boolean. You could make a case for computing the enum on the fly for the sake of easy switches but imho having an if return the errormessage component whenever the error state is not null and returning a loading component when the data state is null (in that order) presents far cleaner and maintainable components. This also discurrages people nesting things too heavily since a lot of developers would probably use a switch with this enum ...
I look forward to hearing peoples responses!

Collapse
farazamiruddin profile image
faraz ahmad Author

This is also an interesting take! So you're saying instead of storing a separate enum string value, just derive the status based on whether or not data or error are not null?

The one case I could argue against this for is re-fetching. Say you have a list of data, and you'd like to re-fetch or fetch more...your original data wouldn't be null...therefore how could you indicate to the user that you were re-fetching / fetching more?

Collapse
kayis profile image
K

I think the basic idea is good, but the implementation is lacking.

An tagged union that holds the error/value would be remove quite some moving parts.

Collapse
farazamiruddin profile image
faraz ahmad Author

Nice, you're the second person to mention tagged unions to me. Looks like I need to do some learning! Thanks for the feedback! 👍

Thread Thread
wierdorohit123 profile image
rohit raut

Useful thing which i have learnt for the day !! Great post

Collapse
mattferrin profile image
Matthew Ferrin

Tagged unions in TypeScript do the same thing, but also allow you to attach a response or error message etc. I love them in combination with switch statements and exhaustive function returns, but a lot of developers react negatively to exhaustive switch statements that handle every possible case. They see them as verbose. I see them as the compiler detecting not only existing logic bugs, but preventing future issues that can arise. Algebraic data types are truly amazing if you embrace them.

Collapse
farazamiruddin profile image
faraz ahmad Author

This sounds interesting. Do you have any examples of this pattern you'd like to share? I'd like to learn more!

I personally have not used tagged unions yet. I'll look into them!

Collapse
fly profile image
joon

Read through the entirety of the series when I noticed how much thought was put into the project structure of the first series.
I was not disappointed. Please keep this up :)

Collapse
farazamiruddin profile image
faraz ahmad Author

Thank you so much! I'm working on my next set of posts currently :)

Collapse
akkisagiraju profile image
Akhil Sagiraju

Since these are mutually dependent state items, I'd use useReducer.

Or to make the code much cleaner, move all this into a separate hook that uses useReducer internally.

Collapse
farazamiruddin profile image
faraz ahmad Author

I agree! I mentioned in the post that I normally wound't use three separate useState calls - just wanted to illustrate the concept easily.

I do something similar to what you're mentioning - which you can read about here if you're interested 👍

dev.to/farazamiruddin/opinionated-...

Collapse
stephyswe profile image
Stephanie

Source code ?

Collapse
farazamiruddin profile image
Collapse
thisismahmoud profile image
Mahmoud

Short and straight to the point. Love it!

Collapse
farazamiruddin profile image
faraz ahmad Author

Thank you Mahmoud!