How to: Asynchronous buttons with loading state in React

Buttons and loading states are one of the most fundamental parts of any app or website. For me at least, I'd press a button to submit information to an endpoint so often that I found myself looking for easy ways to improve this action's UX.

My immediate thought was that it would be great to show a loading state and disable the button until the action had completed. Unfortunately doing this manually for each button looked like far too much boilerplate code.

  1. Add a state to keep track of loading evrey time a button to submit is used
  2. Make sure that before hitting my endpoint I set the loading state to true
  3. Pass the loading state to the button
  4. Ensure that on success or failure the loading state is reset

Doing this for 10 buttons seemed like a code smell, so imagine doing it for my 100+ forms. Not a great idea.

Making your buttons asynchronous and adding a loading state

Funnily enough, the solution is actually incredibly simple. I simply didn't know it could done.

Your button component should look something like this:

const Button = (props) => {
  return (
    <button
      // You don't need this line at this point, but we'll use it later
      onClick={props.onClick}
    >
      {children}
    </button>
  );
};

All we have to do to make our button asynchronous is re-implement its onClick event.

const Button = (props) => {
  const [loading, setLoading] = React.useState(false);

  return (
    <button
      // don't forget to make your onClick async!
      onClick={async (e) => {
        if (props.onClick) {
          // This is the only reliable way to check if a function is asynchronous
          const onClickIsPromise =
            props.onClick.constructor.name === "AsyncFunction";
          // We only set loading if the function is actually async
          // to avoid useless re-renders
          if (onClickIsPromise) setLoading(true);
          // We can await onclick even if it's not asynchronous
          // it won't change its behavior
          await props.onClick(e);
          if (onClickIsPromise) setLoading(false);
        }
      }}
    >
      {children}
    </button>
  );
};

Let's go through how this works:

  1. When onClick is called, we'll check if the prop function is async
  2. If it is, we'll set our loading state to true
  3. Then we'll launch the function itself and await its resolution
  4. Once it's finished we'll set the loading state back to false

That's it! All you have to do now is do something with your new state. For example, you could display an icon and disable the button:

Once you have that first skeleton in place the sky is the limit, you could easily even add some animations to your button to make it look extra professional:

Let me know if this ends up helping you or if you come up with any cool buttons yourself!