UI Architect, Full Stack Web Developer – San Francisco Bay Area (Walnut Creek), CA
UI Architect, Full Stack Web Developer – San Francisco Bay Area (Walnut Creek), CA

useLoadingBar – A Custom React Hook for Rendering a Page Loading/Progress Bar with NProgress and NextJS

Web site visitors tend to be very impatient (Fifty three percent of visits are likely to be abandoned if the pages take longer than 3 seconds to load). You can imagine now frustrating they become when they click on an element and nothing appears to happen, which occurs frequently in a single page application. The page doesn’t repaint. The default behavior of the browser is blocked while JavaScript triggers an API call. Content on the screen changes only after the API response is received, which may take several seconds after a click or tap due to network latency, server load, and many other factors.

One way to appease visitors is to let them know visually that something is happening behind the scenes. Spinners and loading progress bars are typically added for this purpose. NProgress is a popular slim progress bar library that mimics the behavior of mobile browsers as they load content from a site. The bar has a trickle effect that implies incremental loading progress.

NextJS is a React development framework designed for hybrid static & server side rendering, which makes it ideal for ecommerce customers. This library includes a router which include events that we can observe for informing NProgress to begin the ending animation once a page has loaded.

React custom hooks enable developers to extract component logic into reusable functions. Our hook will return an array of 3 elements, a boolean which indicates whether the loading state is active, a function that is invoked when we want to start the progress bar, and an optional function we can call to complete the loading effect.

Here’s the entire hook:

// useLoadingBar.ts
import { useState, useEffect } from 'react';
import NProgress from 'nprogress';
import Router from 'next/router';

const useLoadingBar = (): [boolean, () => void, () => void] => {
    const [isLoading, setIsLoading] = useState<boolean>(false);

    const startLoadingEffect = (): void => {
        setIsLoading(true);
    };
    const finishLoadingEffect = (): void => {
        setIsLoading(false);
        NProgress.done();
    };

    useEffect((): (() => void) => {
        if (isLoading) {
            // Progress bar will be rendered in first visible element with a 
            // .fixed-loading-bar class or, if none exist, the page's body.
            const firstVisibleLoadingBarElement = Array.from(
                window.document.querySelectorAll('.fixed-loading-bar')
            ).filter(
                (s: Element) =>
                    window.getComputedStyle(s).getPropertyValue('display') !==
                    'none'
            )[0];
            const loadingBarParent = firstVisibleLoadingBarElement
                ? `.${firstVisibleLoadingBarElement?.classList?.value
                      .split(' ')
                      .slice(0, 2)
                      .join('.')}`
                : 'body';
            NProgress.configure({
                parent: loadingBarParent,
            });
            NProgress.start();
        }
        Router.events.on('routeChangeComplete', finishLoadingEffect);
        return () => {
            Router.events.off('routeChangeComplete', finishLoadingEffect);
        };
    }, [isLoading]);

    return [isLoading, startLoadingEffect, finishLoadingEffect];
};

export default useLoadingBar;

The hook will search for the first visible DOM element with the class of fixed-loading-bar to place the animated element, which gives the developer flexibility where this element should appear. For example, the developer may want to place the loading bar under the navigation header. If no element exists with that class, the animated progress bar is attached to the HTML body tag and will appear at the top of the viewport.

The hook takes advantage of NextJS’s routeChangeComplete event to trigger the NProgress.done() method. This method completes the animation which causes the loading bar to quickly animate across the screen and fade away, which indicates that the loading process is finished.

This hook returns an array of three helpful variables. The first isLoading is a boolean to indicate when we are animating a loading effect. This is helpful if you need to disable submit buttons, form inputs, or any other UI element in the loading state. The second element startLoadingEffect is a function that triggers the loading effect, which you would call on a route change or before making a fetch request, etc. The third element finishLoadingEffect is the function which completes the loading animation effect. This function is helpful to invoke when API promises resolve or on a particular state change that should convey to the user that a process has completed.

Implementation

You can easily trigger the loading bar animation effect inside your main pages/_app.tsx file:

import useLoadingBar from '@hooks/useLoadingBar';


/**
 * other code here
 */


function MyApp({ Component, pageProps }) {

   const [, startLoadingEffect, finishLoadingEffect ] = useLoadingBar();

   useEffect(() => {
        Router.events.on('routeChangeStart', startLoadingEffect);
        
        return () => {
            Router.events.off('routeChangeStart', startLoadingEffect);
        };
   }, [])
}

You can use the starting and finishing functions to trigger the visual effect from any component that makes an API request. For example:

const getInitialState = useCallback(async () => {
        startLoadingEffect(); // Starts the loading animation
        try {
            await sdk.getAllListItemsAsync();
        } catch (err) {
            setIsAuthError(err === AUTH_ERR);
        }
        refreshListData();
        finishLoadingEffect(); // Completes the loading animation
    }, [finishLoadingEffect, startLoadingEffect]);
    
useEffect(() => {
   if (!listItems.allLists) {
        getInitialState();
   }
}, [getInitialState, listItems])

Once you invoke the startLoadingEffect, the progress bar begins animating from left to right across the screen:

The animation continues slowly across the screen or across the width of the parent which contains the HTML element containing the .fixed-loading-bar class.

When the finishLoadingEffect is invoked, the bar quickly extends the full width of the parent element and fades out of view.

To learn more about NProgress, visit the project page here: https://ricostacruz.com/nprogress/

Leave a comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.