Reusable Loading component in React
Tags: react, typescript, cleancode
Published: 2023-03-17 13:41:45


If you're looking for a way to improve the user experience of your website or application, creating a loading component is a great place to start. In this blog post, I'll explore how to create a reusable loading component in Typescript with React. By leveraging Typescript interfaces, my loading component will be flexible enough to work with any data request in my project.

I'll walk through the process step by step, covering the benefits of using Typescript with React and how it can help me write more robust and maintainable code. 

*Note: The following steps only cover the main classes used and may not include all classes used in the project. For a fully working example, please refer to the GitHub repository at https://github.com/digglp/reactloadingcomponent*


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u0ubsfcds3qx2zm9onef.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kl3qiinvlaiwdos44e3l.png)
#### Step 1: Create a new project using the following command:

`npx create-react-app reactloadingcomponent --template typescript`

This will create a new React starter project.

#### Step 2: Install some dependencies

For this project I will add bootstrap, react-bootstrap and axios.

`npm install bootstrap`
`npm install react-bootstrap`
`npm install axios`

#### Step 3: Set up the folder structure. For my React projects, I typically create the following folders:

-   `src`: The top-level folder for all code
-   `src/domain`: Contains all domain objects
    -   `src/domain/handlers`: Handlers for handling tasks (e.g., getting data)
    -   `src/domain/helpers`: Helper objects (e.g., `DatesHelper`)
    -   `src/domain/models`: Objects to represent data models (e.g., `Weather`)
-   `src/infrastructure`: Contains external dependencies
    -   `src/infrastructure/repositories`: Repositories (e.g., `WeatherAPI`)
-   `src/tests`: Contains all test modules, with the same folder structure as the code
    -   `src/tests/domain`
    -   `src/tests/handlers`
    -   `src/tests/helpers`
    -   `src/tests/models`
    -   `src/tests/infrastructure/repositories`
-   `src/ui`: Contains all React UI code, including components.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/seaxofbjnpgkqfuk6fen.png)


#### Step 4: Create the data handler interface.

Next, let's create the `iHandler` interface that defines the template for all our data calls:

	export interface IHandler {
	  runAsync(request?: any): Promise<any>;
	}
	
#### Step 5: Create the Loader component.

Now, let's create the `Loader` component. The `Loader` component takes in several props, including the `handler` (a handler that the loader will call when loading takes place), `handlerData` (any object that the handler function requires), `onComplete` (a function to call after the data successfully completes), `onError` (a function to call when the data fails), `failureMessage` (a string that represents what message to show on failure), and `canRetry` (a boolean value to determine whether to show the retry button).

The `Loader` component is responsible for loading data, handling errors, and displaying a loading spinner or a retry button. It uses React's `useState` and `useEffect` hooks to manage state and run asynchronous code.

```

	import { useEffect, useState } from "react";
	import { Button, Spinner } from "react-bootstrap";
	import { IHandler } from "../../../../domain/handlers/IHandler";
	
	type Props = {
	  handler: IHandler;
	  handlerData?: any;
	  onComplete: (data: any) => void;
	  onErrored: (error: Error) => void;
	  failureMessage?: string;
	  canRetry: boolean;
	};
	
	export const Loader = (props: Props) => {
	  const [isLoading, setIsLoading] = useState(true);
	  const [isErrored, setIsErrored] = useState(false);
	
	  useEffect(() => {
	    const run = async () => {
	      if (isLoading) {
	        try {
	          props.onComplete(await props.handler.runAsync(props.handlerData));
	          setIsLoading(false);
	        } catch (error: any) {
	          props.onErrored(error);
	          setIsLoading(false);
	          setIsErrored(true);
	        }
	      }
	    };
	
	    run();
	  }, [isLoading, props]);
	
	  const retryAsync = async () => {
	    setIsLoading(true);
	    setIsErrored(false);
	  };
	
	  return (
	    <>
	      {isLoading && (
	        <Spinner animation="border" role="status">
	          <span className="visually-hidden" data-testid="loading">
	            Loading...
	          </span>
	        </Spinner>
	      )}
	      {isErrored && (
	        <div>
	          {props.failureMessage ? props.failureMessage : <span>"Error loading data"</span>}
	          {props.canRetry && (
	            <Button className="mb-3" variant="secondary" onClick={() => retryAsync()} data-testid="retryButton">
	              Retry
	            </Button>
	          )}
	        </div>
	      )}
	    </>
	  );
	};

```
##### Some explanations.

###### State and useEffect Hook

The initial state `isLoading` is set to true. When the `useEffect` hook runs and `isLoading` is true, the asynchronous call to the network is executed using `props.handler.runAsync(props.handlerData)`. If the call returns without errors, the `props.onComplete` function is called and the data returned from the handler is passed back. In case of an error, the `try catch` block handles it by calling the `props.onErrored` function and setting the `isErrored` state to true.

###### Retry 

If `props.canRetry` is true and `isErrored` state is true, the retry button will be displayed. Clicking the button will trigger the `retry` function, which sets the `isLoading` state to true. This will cause the `useEffect` hook to execute and attempt loading again.

###### iHandler functionality

The `iHandler` interface allows any object that implements it to use this loading component, making it reusable across all data calls in your application.


#### Step 6: Lets use the loader

To demonstrate how the loader component can be used, we'll create a new component and handler to load character data from the Rick And Morty API ([https://rickandmortyapi.com/](https://rickandmortyapi.com/)).

For this, we'll need the following bits of code:

**Repository Code**

-   `IRickAndMortyCharacterRepository.ts` - a repository interface
-   `RickAndMortyCharacterRepository.ts` - an implemented repository class
-   `IReadRepository.ts` - a base read repository interface
-   `BaseReadRepository.ts` - an implemented class that handles the Axios request/response
-   `RepositoryConfigs.ts` - a config class that handles environment variables, such as the URL for the API we want to call

**Handler Code**

-   `IHandler.ts` - an interface that works with the Loader component
-   `CharacterListHandler.ts` - a class that implements `IHandler.ts`

_The above structure enables straightforward unit testing by supplying a mock repository to the handler. See the testing section below for more details._


###### BaseReadRepository.ts

```
import axios, { AxiosRequestConfig } from "axios";

import { IReadRepository } from "./IReadRepository";

export class BaseReadRepository<T> implements IReadRepository<T> {
  async getDataFromUrlAsync(url: string): Promise<T> {
    const requestConfig = {
      headers: {},
      timeout: 10000,
    } as AxiosRequestConfig;

    const response = await axios.get(url, requestConfig);
    const data = response.data;

    return data;
  }
}
```

The purpose of the BaseReadRepository class is to handle asynchronous read requests. This code uses the axios library to send a GET request to a specified URL and returns the response data. Other repository classes can utilize this implementation to handle read requests in a consistent manner.

###### RepositoryConfig.ts

```
export class RepositoryConfigs {
  static configuration = {
    development: {
      characterUrl: "https://rickandmortyapi.com/api/character",
    },
    beta: {
      characterUrl: "https://rickandmortyapi.com/api/character",
    },
    production: {
      characterUrl: "https://rickandmortyapi.com/api/character",
    },
  } as any;

  static getCharacterUrl(environment: string) {
    return this.getUrl(environment, "characterUrl");
  }

  private static getUrl(environment: string, url: string) {
    if (environment) return RepositoryConfigs.configuration[environment][url];
    else throw new Error("No environment found");
  }
}
```

The `RepositoryConfigs` class provides a simple way to store environmental variables. An example of how to access a stored variable is by calling `RepositoryConfigs.getUrl('beta', 'characterUrl')`.

###### RickAndMortyCharacterRepository.ts

```
import { IRickAndMortyCharacterRepository } from "./IRickAndMortyCharacterRepository";
import { RepositoryConfigs } from "./../RepositoryConfigs";
import { BaseReadRepository } from "../BaseReadRepository";
import { ResponseSchema } from "../../../domain/models/ResponseSchema";

export class RickAndMortyCharacterRepository
  extends BaseReadRepository<ResponseSchema>
  implements IRickAndMortyCharacterRepository
{
  url;

  constructor(environment: string) {
    super();
    this.url = RepositoryConfigs.getCharacterUrl(environment);
  }

  async getCharactersAsync(pageNumber: number): Promise<ResponseSchema> {
    const url = `${this.url}?page=${pageNumber}`;
    const characters = await this.getDataFromUrlAsync(url);

    return characters;
  }
}
```

The `RickAndMortyCharacterRepository` class extends the base class `BaseReadRepository` and uses its method `getDataFromUrlAsync(url)` to make a request to the Rick and Morty character API. This class encapsulates the details of calling the API, such as constructing the URL with the specific endpoint and capturing the page number. An example usage of this class would be to call `const characters = await new RickAndMortyCharacterRepository().getCharacters(pageNumber);` to retrieve a page of characters.


###### CharacterListHandler.ts

```
import { IRickAndMortyCharacterRepository } from "./../../../infrastructure/repositories/rickandmortyrepository/IRickAndMortyCharacterRepository";
import { IHandler } from "../IHandler";
import { CharacterListRequest } from "../../models/CharacterListRequest";

export class CharacterListHandler implements IHandler {
  constructor(private rickAndMortyCharacterRepository: IRickAndMortyCharacterRepository) {}
  async runAsync(characterRequest: CharacterListRequest): Promise<any> {
    try {
      const response = await this.rickAndMortyCharacterRepository.getCharactersAsync(characterRequest.pageNumber);
      if (characterRequest.simulatedDelayMs > 0) await this.wait(characterRequest.simulatedDelayMs);

      return response.results;
    } catch (error: any) {
      throw new Error("Error getting character data");
    }
  }

  wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
}
```
The `CharacterListHandler` class implements the `IHandler` interface and takes an instance of the `IRickAndMortyCharacterRepository` interface as a parameter. It defines a method `runAsync` that takes a `CharacterListRequest` object and returns a Promise. The `runAsync` method calls the `getCharactersAsync` method of the injected repository to retrieve a list of characters, with an optional simulated delay. If there is an error, it throws an error with a generic message. The class also contains a private `wait` method to pause execution for a specified number of milliseconds.

###### CharacterList.tsx

```
import React, { useEffect, useState } from "react";
import { Button, Card } from "react-bootstrap";
import { CharacterListHandler } from "../../../domain/handlers/RickAndMortyHandlers/CharacterListHandler";
import { Character } from "../../../domain/models/Character";
import { CharacterListRequest } from "../../../domain/models/CharacterListRequest";
import { RickAndMortyCharacterRepository } from "../../../infrastructure/repositories/rickandmortyrepository/RickAndMortyCharacterRepository";
import { useHelper } from "../../hooks/useHelper";
import { Loader } from "../utilities/loader/Loader";

export const CharacterList = () => {
  const helper = useHelper();
  const [loading, setLoading] = useState(true);
  const [characterData, setCharacterData] = useState<Character[]>([]);
  const [pageNumber, setPageNumber] = useState(1);

  const characterListHandler = new CharacterListHandler(new RickAndMortyCharacterRepository(helper.getEnvironment()));

  const onLoadingComplete = (data: any) => {
    // processDepots(data);
    console.log("Data loaded: ", data);
    setCharacterData(data);
    setLoading(false);
  };
  const onLoadingErrored = (error: Error) => {
    console.log(error.message);
  };

  useEffect(() => {
    setLoading(true);
  }, [pageNumber]);

  return (
    <>
      <Card>
        <Card.Header>Character loader with 5 second delay</Card.Header>
        <Card.Body>
          {loading && (
            <Loader
              handler={characterListHandler}
              handlerData={new CharacterListRequest(500, pageNumber)}
              onComplete={onLoadingComplete}
              onErrored={onLoadingErrored}
              failureMessage="Error loading character data"
              canRetry={true}
            />
          )}
          {!loading && characterData && (
            <>
              <Button className="me-3" onClick={() => setPageNumber(pageNumber > 0 ? pageNumber - 1 : pageNumber)}>
                Load previous page
              </Button>
              <Button onClick={() => setPageNumber(pageNumber + 1)}>Load next page</Button>
              <h3>Character Data Loaded</h3>
              <ul>
                {characterData.map((character) => (
                  <li key={character.id}>
                    {character.name} - {character.location.url}
                  </li>
                ))}
              </ul>
            </>
          )}
        </Card.Body>
      </Card>
    </>
  );
};
```

The `CharacterList` react component uses the `Loader` component to load data from the Rick And Morty character api, and demonstrates how to manage loading state. It dynamically updates the `Loader` component based on user interaction, by re-initializing with new parameters when the page number changes. The loaded data is displayed as a list of characters with their names and location URLs.


#### Step 7: Writing some unit tests


```
import { RepositoryConfigs } from "../../infrastructure/repositories/RepositoryConfigs";
import { CharacterListHandler } from "../../domain/handlers/RickAndMortyHandlers/CharacterListHandler";
import { RickAndMortyCharacterRepository } from "../../infrastructure/repositories/rickandmortyrepository/RickAndMortyCharacterRepository";
import axios from "axios";
import {
  setupTestEnvironment,
  restoreEnvironment,
  mockResolvedValue,
  Verb,
  testEnvironment,
  getAxiosHeaders,
  mockRejectedValue,
} from "./TestBase";
import { CharacterListRequest } from "../../domain/models/CharacterListRequest";

jest.mock("axios");

describe("Character List API Test Suite", () => {
  beforeEach(() => {
    setupTestEnvironment();
  });

  afterEach(() => {
    restoreEnvironment();
  });

  it("should return character data", async () => {
    mockResolvedValue(getValidCharacterDataResponse(), Verb.GET);

    const rickAndMortyCharacterRepository = new RickAndMortyCharacterRepository(testEnvironment);
    const characterListHandler = new CharacterListHandler(rickAndMortyCharacterRepository);

    const characterList = await characterListHandler.runAsync(new CharacterListRequest(0, 0));

    expect(axios.get).toHaveBeenCalledWith(
      expect.stringContaining(RepositoryConfigs.getCharacterUrl(testEnvironment)),
      getAxiosHeaders()
    );
    expect(characterList).toEqual(getValidCharacterDataResponse().data.results);
  });
  it("should throw error when network fails", async () => {
    mockRejectedValue(new Error("Test axios error"), Verb.GET);
    const rickAndMortyCharacterRepository = new RickAndMortyCharacterRepository(testEnvironment);
    const characterListHandler = new CharacterListHandler(rickAndMortyCharacterRepository);

    await expect(characterListHandler.runAsync(new CharacterListRequest(0, 0))).rejects.toThrow(
      "Error getting character data"
    );
  });
});

const getValidCharacterDataResponse = () => {
  return {
    data: {
      info: {
        count: 671,
        pages: 34,
        next: "https://rickandmortyapi.com/api/character/?page=2",
        prev: "",
      },
      results: [
        {
          id: 1,
          name: "Rick Sanchez",
          status: "Alive",
          species: "Human",
          type: "",
        },
        {
          id: 2,
          name: "Morty Smith",
          status: "Alive",
          species: "Human",
          type: "",
        },
      ],
    },
  };
};
```

I have created some basic unit tests to demonstrate how to test API calls. These tests are located in the integration folder because they test the interaction between the handler and the repository. The tests use mock axios responses. The positive test ensures that the handler is calling the correct URL and returns the mocked data, while the negative test tests error handling. Note that this is not a comprehensive test suite but rather serves as an example of how to mock repository interfaces.

#### Summary

The React Loading Component is a reusable React component designed to handle data loading from external APIs. It provides a loading indicator while data is being retrieved and displays an error message if the data retrieval fails. It also allows for easy retrying of data retrieval and supports custom error messages. The component can be easily configured and customized to fit various use cases. The code is open-source and available on GitHub.

I am new to React and TypeScript and still learning. If you have any suggestions or improvements, please feel free to add them to the comments or contribute to the GitHub repository. If you made it this far, well done and thank you for reading.