DEV Community

Neil Chen
Neil Chen

Posted on • Updated on

A Better Way to Design a Loading Button Component

UI Feedback for Loading

When a user clicks a submit button that sends a request, we need to provide a loading UI feedback to inform the user that the backend is processing the request. A simple way to implement this is to have a global overlay loading animation that blocks the user's actions and makes them wait. However, this implementation can make users feel that they are not in control. To improve the user experience, we usually choose to have a button that can turn into a loading state.

Loading Buttons

The Burden of Repetitive Work

Handling requests and making each button change between loading and enabled states can be quite annoying. Although there are solutions like the useMutation hook in React Query, which can wrap the implementation details of the fetching and return the isLoading state for you to pass to the button props, sometimes we don't want to add another layer of fetching with hooks, or we want to make a pure request using other libraries like Redux or pure axios fetch. In such cases, we can use the following technique to make our lives easier.

Loading Button Component Design

We can create a custom button component with the following code:

import { useState } from 'react';

interface LoadingButtonProps {
  isLoading?: boolean;
  onClick: () => void | Promise<void>;
  text?: string;
}

const LoadingButton = (props: LoadingButtonProps) => {
  const { isLoading, onClick, text } = props;

  const [isInternalLoading, setIsInternalLoading] = useState(false);

  const handleClick = async () => {
    setIsInternalLoading(true);
    try {
      await onClick();
    } finally {
      setIsInternalLoading(false);
    }
  };

  if (isLoading || isInternalLoading) {
    return <button disabled>Loading...</button>;
  }

  return <button onClick={handleClick}>{text}</button>;
};
Enter fullscreen mode Exit fullscreen mode

We can then use this custom button component as follows:

const ExampleView = () => {
  return (
    <>
      <p>
        The Loading Button will change the loading state based on async function calling.
      </p>
      <LoadingButton
        onClick={async () => {
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

The Loading Button component will change its loading state automatically, and we also have the flexibility to explicitly pass the isLoading state to this component to fulfill other situations.


const ExampleView2 = () => {
  const [isLoading, setIsLoading] = useState(false);

  return (
    <>
      <p>
        The Loading Button will change the loading state based on the passed props.
      </p>
      <LoadingButton
        isLoading={isLoading}
        onClick={() => {
          console.log('sync function calling');
        }}
      />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Today I share a simple pattern to make fetch loading handling easier. I hope this little technique can help you to organize your code better and inspire you for other use cases!

Top comments (2)

Collapse
 
peiqun profile image
Pei-Qun

Simple but useful 🤩

Collapse
 
neil585456525 profile image
Neil Chen

Thx! Hope it help.