DEV Community

AyoAlfonso
AyoAlfonso

Posted on • Updated on

Implementing A Global Progress Bar In React

I am currently working on a react code base where we use a lot of tables, these tables sometimes take an unusual long time to load, mainly because the back-end doesn't send paginated data to the front-end. I personally feel there is no reason this should exist on a code base.

For three(3) reasons;

On the back-end it means the database round trips will increase exponentially as user requests increase, there will surely be database I/O locks.

If a cache were to be implemented the cache will be "red-hot", the server RAM will suffer for this also because of the sheer size of the data involved.

On the front-end level it encourages bad practices, by that I mean to say a lot of libraries are configured or built to work with pagination or moderate data, this is usually done with the best intentions in my opinion, to save developers a lot of grunt work.

Any pattern you implement that is one level below the tried and tested pattern of incrementally loading data will start reflecting as inefficiencies in our code base. Also it is important to note that code will have to be moved around a lot if in the future the teams decides to paginate data.

So why not now?

However, there are time and resource constraints, and yes projects do end up like this. To help the UX for the user of the platform I needed to implement the loader, there was an existing loader but I had major issues with it.

Firstly the loader didn't take into account what was happening on the network side of things, so if I made a request I want to see some feedback that the request is in fact being sent to some server somewhere or not.

Also I don't want the network updates to be inconsistent with the state of my component, in the sense that; I don't want my request to fail and then the loader is still spinning or showing some sort of progress.

It needs to show me what I need to see and it needs to show me fast. Another thing was I didn't want to have to write a lot of code for it to work. I just want it to be a few lines of code if that is possible.

I started working on it, I went with making adding a isLoading prop in the state manager, false by default then passed it a true boolean when I got data. This worked as so :

export function uploadImage(data){
    const request = axios.post('utilityserv/api/v1/upload_image', data);

    return (dispatch) => {
        dispatch(showImageUploading())
        request.then((response) =>{
            dispatch(showImageUploaded())
            dispatch(showMessage({message: "New image upload sent to server successfully"}))
                Promise.all([
                    dispatch({
                        type: UPLOAD_IMAGE,
                        payload: response.data
                    })
                ]).then(() => dispatch(Actions.getAllImages()))
        });   
    }  
}
Enter fullscreen mode Exit fullscreen mode

Let us do a breakdown of what is happening up here; an action creator with the name uploadImage is used to initiate the process of uploading an image, we can see that this a good example because it takes longer for high res images to finish the upload process.

On pages like this the developer absolutely wants the feedback, for more critical applications [financial apps for example], we might have to deal with a user retrying an action that should only happen once.

Of course idempotency on the back-end can help prevent this from happening but doing things properly right from the UX of your application is miles better than leaving it till it gets to the back-end. We dispatch another action creator called showImageUploading, as so in the code below.

export function showImageUploading(){
    return (dispatch) => {
        dispatch({
            type: CMS_IMAGE_UPLOADING,
        })
    }
}

export function showImageUploaded(){
    return (dispatch) => {
        dispatch({
            type: CMS_IMAGE_UPLOADED,
        })
    }
}

Enter fullscreen mode Exit fullscreen mode

With the help of the simplified naming we can see what these two action creators do. The object with CMS_IMAGE_UPLOADING as its type is dispatched to the reducer and consequentially on the front-end the prop called isloading changes to true and the div containing the table is hidden and replaced by the div containing the loader (which is just an element, image or whatever, that is always moving).

This can be done with an ternary operation to keep the code neat as so.

 this.props.isLoading ? 
   <div className={classes.root}>
               <Loader type="Oval" color="#039be5" height={60} width={60} timeout={5000} />
   </div>
   : (<div> Your table will sit here </div>)
Enter fullscreen mode Exit fullscreen mode

What is left in this process for everything to come together is to tell the loader to go away once our response is back, reducer file where we can close the loader will most definitely contain something like this;

   case Actions.CMS_IMAGE_UPLOADING:
            {
                return {
                    ...state,
                    imageUploading: true
                }
            }
        case Actions.CMS_IMAGE_UPLOADED:
            {
                    return {
                        ...state,
                        imageUploading: false
                    }
            }
Enter fullscreen mode Exit fullscreen mode

It is all straight forward so far so good and if we are being honest anyone can implement this in a few minutes as depending on what your project structure is like , however you want to be able to do this without these amount adding lines of code to all your Redux files (both reducer and action files).

It is important to note that you want to be able to do update the state management of your API calls, say there is a new response you want to handle in a certain way with the progress bar, typical examples will be picking header data, error handling, or maybe show progress accurately.

Earlier we set out by listing all the things we want our pattern to be able to do;

  1. Consistency with the state of network requests
  2. Needs to be fast
  3. Very little or no boilerplate code
  4. Use (Read, intercept, or modify) the data on the actual requests

First thing to do is to locate where you are initiating your app. To find this shouldn't be too hard especially because most apps have similar patterns at that level, what you're looking for is the top level app class that houses the other components.

You can do a quick search through your code base if you don't know the name of the app with : App = () => {}, if that doesn't work, it probably means your entry app class doesn't have the standardized naming and that is not a bad thing except of course this can really confuse people new to the code base.

If finding it by above method doesn't work, the full proof method to actual find this without fail is to go to the entry file and enter the app component. That is the component we need

const render = () => {

    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
};
Enter fullscreen mode Exit fullscreen mode

NB: The above method is better than searching for the app component if you're not familiar with the code base.

Now once you have located the component you can add this to the componentWillMount function as it will do two thin

componentWillMount(){
        const self = this
        axios.interceptors.request.use(function (config) {

         }, error => {

        });

         axios.interceptors.response.use(function (response) {

        }, error => {

        });
      }
Enter fullscreen mode Exit fullscreen mode

The above takes the Axios lib immediately it is bootstrapped and on the use function that is a part of the Axios library for extending its interceptor behavior, we get to do whatever we intend to do, for example we can add a counter here to keep track of the total number of requests made from the front-end and eventually develop some insights into how many are failing compared to those passing and to deliver a better product, capture the reason for the failure.

The errors can also be caught inside this extensions and handle separate from the rest of the code base if that's needed. The next thing to do is to create our one time Redux actions and action creators that will be working with the code above (incomplete).

And so for me at the top level I decided to create a loader.action.js file that will hold the action creators

export const  AXIOS_REQ_UPLOADED  = '[LOADER] CLOSE';
export const AXIOS_REQ_UPLOADING = '[LOADER] SHOW';

export function axiosRequestUploading()
{
    return {type: AXIOS_REQ_UPLOADING }
}

export function axiosRequestUploaded()
{
    return {type: AXIOS_REQ_UPLOADED}
}

Enter fullscreen mode Exit fullscreen mode

The code above is then consumed by a newly created loader.reducer.js that is exported to the App via a global store. This is important because if you attach this to the global store, you will be able to pull it into any component by calling the mapStateToProps().

Code base patterns are different but the most likely scenario is that your store is initiated with a state management lib like Redux in a file of its own and that is where you import the reducer for this into.


const initialState = {
    axiosReqUploading: false,
};

const axiosLoader = function (state = initialState, action) {
    switch ( action.type )
    {

        case Actions.AXIOS_REQ_UPLOADING:
            {
                return {
                    ...state,
                    axiosReqUploading: true
                }
            }
        case Actions.AXIOS_REQ_UPLOADED:
            {
                    return {
                        ...state,
                        axiosReqUploading: false
                    }
            }
        default:
        {
            return state;
        }
    }
};

export default axiosLoader;

Enter fullscreen mode Exit fullscreen mode

To complete this function we need to add the action creators so that they can be called at the two possible scenarios. One is just at the point where the request is about to be made, it returns the config which contains all manner of information about the request to be made, both constructed by axios and by the developer, the other is triggered when the response is back from the request.

    componentWillMount(){
        const self = this
        axios.interceptors.request.use(function (config) {
          self.props.axiosRequestUploading()
          return config
         }, function (error) {
           return Promise.reject(error);
         });

         axios.interceptors.response.use(function (response) {
           self.props.axiosRequestUploaded()
          return response;
        }, function (error) {
          return Promise.reject(error);
        });
      }
Enter fullscreen mode Exit fullscreen mode

Now that requests can be made and received now. How do we use this on the front-end itself, for that we can use the

 this.props.axiosReqUploading ? 
   <div className={classes.root}>
               <Loader type="Oval" color="#039be5" height={60} width={60} timeout={5000} />
   </div>
   : (<div> Your table will sit here </div>)
Enter fullscreen mode Exit fullscreen mode

For me because I am currently using the google material theme and I like the components I am working with;

/**  Loader.js **/
  <Fade
    in={this.props.axiosReqUploading}
    style={{
    transitionDelay: this.props.axiosReqUploading ? '800ms' : '0ms',
    }}
    unmountOnExit >
        <LinearProgress variant="query" />
 </Fade>

Enter fullscreen mode Exit fullscreen mode

Main frotend code

    import Loader from "/.Loader.js"
   <Loader/>
   <div> Your table will sit here </div>
Enter fullscreen mode Exit fullscreen mode

You will notice how I don't use an ternary to wrap the table itself, I prefer this because I have abstracted a lot of code and I only have to work with two lines of code whenever I need to add my new component to a new page. Hopefully this turns out to be useful for you beyond this implementation.

Like I have mentioned throughout this article, there are many interesting use cases especially around monitoring, gathering the insights and delivering good UX to the user. Thanks for reading!

Top comments (0)