Adding A Reusable Loading Indicator To React Components

Alexander Paterson
|
Posted almost 7 years ago
|
12 minutes
Adding A Reusable Loading Indicator To React Components
Every asynchronous application needs to communicate to users when they're supposed to be waiting

One of the best things you can do to make your development more efficient is write reusable code. I quickly figured out that every application I built needed loading indicators, and that's why this github repository/npm package exists

Here, I'm going to guide you through the development of my system for controlling components that need to go in and out of a loading state, so you're not forced to use my code. 

Functionality

react-loading-indicator-component offers two different means to control when individual components are in a loading state:

  1. Components wrapped in the higher-order-component (HOC) LoadingComponent can call this.props.startLoading(loadingText) and this.props.endLoading()
  2. The LoadingComponent HOC can be initialised with an ID, which allows you to control the loading status of the component from the global state. I provide a reducer and some action generators to achieve this.

Components wrapped with the LoadingComponent HOC have the benefit of not losing state while they're loading. The component isn't actually being removed from the DOM, and it won't lose its instance variables. 

The Higher-Order Component

To begin with, we're going to design a HOC that wraps around any component that needs to be capable of going into a loading state. This will take the form of a function that returns a component, and it will take three arguments: 

  1. The component to be wrapped
  2. The component that will be shown while the component is loading (the Loader)
  3. An array of loading id's, which allow us to control the loading status of a component through our global state. We use an array so that we can have classes of loadable components in a many-to-many relationship

Actually, the loading status of loadable components is always controlled by the global state; usually, however, it's through a randomly generated ID, which is always present, but not known to the developer due to its indeterminate nature. 

Also, we're not just going to return a wrapped component, we're also going to connect it to state.loading with Redux.connect first.

With all this given, the code I'm about to show you should be very easy to understand. The only other piece of information that you need to know is that the functions addLoading and endLoading are simply action generators that, when computed and passed to dispatch, set state.loading[alertId] to {text, isLoading: true} if loading, and {text, isLoading: false}, otherwise.

// app/components/shared/HOC/LoadingComponent.jsx

import React from 'react';
import {connect} from 'react-redux';

import {startLoading, endLoading} from '../../../actions/loadingActions';

var LoadingComponent = function(ComposedComponent, Loader, loadingIdArray) {
  var LoadingComponentClass = React.createClass({
    componentWillMount() {
      var loadingId = uuid.v4()
      this.loadingId = loadingId;
      this.loadingIdArray = loadingIdArray ? [loadingId, ...loadingIdArray] : [loadingId];
    },

    handleStartLoading(loadingText) {
      this.props.dispatch(startLoading(this.loadingId, loadingText));
    },

    handleEndLoading(loadId) {
      this.props.dispatch(endLoading(loadId ? loadId : this.loadingId));
    },

    render() {
      var {loading} = this.props;
      var {loadingId, loadingIdArray} = this;

      var passToChild = {
        startLoading: this.handleStartLoading,
        endLoading: this.handleEndLoading,
        loadingIdArray,
        loadingId
      }

      var loadingObject;
      // loadingObject will be null or, if loading, set to an object structured:
      //   { text, isLoading }
      // Specifically from the last applicable "loading" state set.
      // We need both of these pieces of information later.
      loadingIdArray.forEach((id) => {
        var loadObj = loading[id];
        if (loadObj && loadObj.isLoading) {
          loadingObject = loadObj;
        }
      });

      var isLoading = false;
      if (loadingObject && loadingObject.isLoading) {
        isLoading = true;
      }

      var styleOne = !isLoading ? {display: 'none'} : {};
      var styleTwo = isLoading ? {display: 'none'} : {};

      return (
        <div style={{width: '100%'}}>
          <div style={{width: '100%', ...styleOne}}>
            <Loader loadingText={loadingObject ? loadingObject.text : "Loading..."}/>
          </div>
          <div style={{width: '100%', ...styleTwo}}>
            <ComposedComponent {...this.props} {...passToChild}/>
          </div>
        </div>
      )
    }
  });

  function mapStateToProps(state) {
    return { loading: state.loading };
  }

  return connect(mapStateToProps)(LoadingComponentClass);
}

The above is very simple; starting from the bottom with the render method.

All we're doing in the render method is finding out from the state whether or not our component should be loading. We check every id in our component's loadingIdArray attribute to see if this.state.loading[id].isLoading is set to true. If multiple id's in this.loadingIdArray are set to true, we'll just consider the last one in this.state.loading when we refer to this.state.loading[id].text.

handleStartLoading and handleEndLoading simply dispatch the startLoading and endLoading actions (which we haven't defined yet), and on componentWillMount we assign the component its loadingId and store its loadingIdArray with the new ID appended.

Now to understand how the action generators startLoading and endLoading work, we must first define our reducer.

Reducer 

loadingReducer is going to designed to use as a part of a combineReducers call as follows:

// app/reducers/index.js

import {combineReducers} from 'redux';
import {loadingReducer} from './loadingReducer.jsx';

export default combineReducers({
  loading: loadingReducer
});

To handle a START_LOADING action, it'll set [action.id] to an object with a property text set to action.text and isLoading set to true. In the case of END_LOADING it'll set the same isLoading property to false:

// app/reducers/loadingReducer.jsx

export var loadingReducer = (state = {}, action) => {
  switch (action.type) {
    case 'START_LOADING':
      return {
        ...state,
        [action.id]: {
          isLoading: true,
          text: action.text
        }
      };

    case 'END_LOADING':
      return {
        ...state,
        [action.id]: {
          isLoading: false
        }
      };

    default:
      return state;
  }
}

Actions

Our action generators are nothing special at all. They are defined as follows:

// app/actions/loadingActions.jsx

export var startLoading = (id, text) => {
  return {
    type: 'START_LOADING',
    id,
    text
  };
};

export var endLoading = (id) => {
  return {
    type: 'END_LOADING',
    id
  };
};

And that's essentially it. With that all set up, we're now ready to put our code to use.

Usage

With your reducer and store all set up, we just need to design a Loader component, wrap any desired components in LoadingComponent, and dispatch some actions to prove things work. 

Our Loader component is going to be super simple; there's no reason for us to complicate it here. It can be anything: it's a component that can be as complex as any other. Usually I just use an animated spinner with the text beneath it.

// app/components/shared/Loader.jsx

import React from 'react';

export var Loader = React.createClass({
  render: function() {
    var {loadingText} = this.props;
    return (
      <div>
        <span>{loadingText}</span>
      </div>
    );
  }
});

Now I'll define the component we're going to make loadable. It's going to contain a button that triggers itself to load, and it's called Main.

// app/components/Main.jsx

import React from 'react';
import {connect} from 'react-redux';

import {startLoading} from '../actions/loadingActions.jsx';


export var Main = React.createClass({
  doStartLoading: function() {
    var {endLoading} = this.props;

    // Two options for controlling LoadingComponents:

    // Use the startLoading method of the higher-order component, passed down
    // as a prop. This takes one argument: the loadingText.
    // var {startLoading} = this.props;
    // startLoading("I am loading!");


    // Alternatively, initialise LoadingComponents with ID's from an enumeration,
    // and manually dispatch actions from the action creator, startLoading, a function from the
    // react-loading-indicator-component library.
    var {dispatch} = this.props;
    dispatch(startLoading("MAIN_LOADER", "I am loading!"));
    setTimeout(function() {
      console.log("setTimeout: It's been one second, stop loading!");
      endLoading("MAIN_LOADER");
    }, 1000);
  },
  render: function() {
    var {handleStartLoading} = this.props;
    return (
      <div>
        Hello!
        <button onClick={this.doStartLoading}>Start Loading</button>
      </div>
    );
  }
});
module.exports = connect()(Main);

Finally, we render the Main component as a part of our application:

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';

var store = require('./store').configureStore();

import Main from './components/Main';
import Loader from './components/shared/Loader';
import LoadingComponent from './components/shared/HOC/LoadingComponent';

var MainComponent = LoadingComponent(Main, Loader, ["MAIN_LOADER", "ALL_LOADER"]);

ReactDOM.render(
  <Provider store={store}>
    <div>
      <MainComponent />
    </div>
  </Provider>,
  document.getElementById('app')
);

And there you have it. A component that can trigger its own loading mechanism. 

There are many ways to improve on this implementation; for example, this code's not very light-weight when we consider that we're always wrapping our components in at least two div tags. 



-->
ALEXANDER
PATERSON