How to Add a Loading Component in Typescript with React Hooks

When visiting a website, the client loads information from the server to display. While the server responds, most web pages display a loading spinner or similar animation. The following demonstration uses React hooks to implement a loading component that displays while the client loads the results of a function call.

A useEffect React hook calls a function that contains the API call to the server. This is important because the client can determine exactly when the API request is complete and access that data. There are two main pieces of this system: a helper function and a useEffect React hook.

The Helper Function

The helper function wraps the useEffect hook so we can potentially reuse its logic on many different API calls. For this post, it will be called useRequest. This function returns two things: an isLoading state and the data from our API call. The isLoading state will be used to control our loading spinner later in the tutorial. In a file named “request-hook.ts”, add the following code:


const useRequest = <T>(request: () => Promise<T>
): [boolean, T | undefined] => {}

The function above can be defined with a type, T, and it will return an array with the isLoading boolean and the data T if the API call is successful. The useRequest function takes a function as a parameter. This function should return the data from the server and is the function useEffect will call. Structures to handle error cases can be added in the future. An example of calling this function is as follows:

 const [isLoading, data] = useRequest(getData); 

getData can be any function that returns an object.

useEffect Hook

Now the useRequest function’s body will be filled in. First instantiate the hooks that will store the data from the server.

const useRequest = <T>(
request: () => Promise<T>
): [boolean, T | undefined] => {
const [data, setData] = useState<T | undefined>(undefined);
const [loading, setIsLoading] = useState(true);
}

These useState hooks will keep track of the data we get from the server and will tell us if we’ve completed our request. Next, add in the useEffect hook.


export const useRequest = (request: () => Promise): [boolean, T | undefined] => {
    const [data, setData] = useState(undefined);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        const fetchData = async (): Promise => {
            setIsLoading(true);
            setData(undefined); // clear the data from any previous runs
            const result = await request();
            setData(result.data); // store the new data
            setIsLoading(false); // clear the loading state
        };

        void fetchData();
    });

    return [isLoading, data];
};

UseEffect takes a callback function and the function will run each time React renders. This callback contains a series of semaphores that track the state of the request. isLoading will prevent the component that displays the data from loading too early. The data hook will contain the data from the server.

The Display and Loading Components

Components can now pass the useRequest hook a function to get data from the server. Add the code below to a new file called “data-display-page.tsx”. For the imports to work, “request-hook.ts” and “data-display-page.tsx” should be in the same directory. If the files are not, update the import to be the relative path to your file.  I used the CircularProgress component from material UI to get the spinner component. However, any component will render.


import { useRequest } from './request-hook';

export const DataDisplayPage: React.FC = () => {
    const [isLoading, data] = useRequest(getData);
     return ({isLoading ? ("Spinner") :!data ? ("DisplayError") : (data)});
};

If you are unfamiliar with a ternary operator, the code inside the return statement, here is a link to the docs.

This component will call the useRequest hook that we created while React renders the page. When the page first displays, the loading spinner will appear and it will be there until the server responds. Once the loading completes, React will look at the data we received. If the request returns undefined, then an error displays and the page renders. If the request succeeds, then the data on the response will display on the page. This useRequest hook accepts any function that returns a promise. The useRequest hook executes the function when the page initially loads.

Below is the completed code for the entire tutorial.

“request-hook.ts”:


const useRequest = <T>(
request: () => Promise<T>
): [boolean, T | undefined] => {
     const [data, setData] = useState<T | undefined>(undefined);
     const [isLoading, setIsLoading] = useState(true);
     
     useEffect(() => {
          const fetchData = async (): Promise => { 
          setIsLoading(true);
          setData(undefined); // clear the data from any previous runs
          const result = await request();
          setData(result.data); // store the new data 
          setIsLoading(false);  // clear the loading state
     });
     return [isLoading, data];
};

“data-display-page.tsx”:


export const DataDisplayPage: React.FC = () => {
     const [isLoading, data] = useRequest(getData);
     return ({isLoading ? ("Spinner") :!data ? ("DisplayError") : (data)});
};

This should act as a good starting point to make a frontend that relies on different API requests for different components. I encourage replacing the “spinner” and “display error” text on the display page with components that match the theme of the frontend.

These three different pieces work together to create a bridge between the content displayed on the frontend and the requests used to communicate with the server. They also help limit the amount of duplicated logic by containing the loading state in one component. This makes adding new web pages more modular and easier to implement.