Opinionated React - Use Status Enums Instead of Booleans

farazamiruddin

faraz ahmad

Posted on July 6, 2020

Opinionated React - Use Status Enums Instead of Booleans

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

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

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 !== ''
Enter fullscreen mode Exit fullscreen mode

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

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

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.

💖 💪 🙅 🚩
farazamiruddin
faraz ahmad

Posted on July 6, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related