A Better Way to Design a Loading Button Component
Tags: react, webdev, javascript, typescript
Published: 2023-04-17 15:15:58

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/906zs6c045lftzi9v2ko.jpg)

## 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:

```tsx
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>;
};
```

We can then use this custom button component as follows:

```tsx
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));
        }}
      />
    </>
  );
};
```

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.

```tsx

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');
        }}
      />
    </>
  );
};
```
## 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!