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:
- Components wrapped in the higher-order-component (HOC) LoadingComponent can call
this.props.startLoading(loadingText)
andthis.props.endLoading()
- 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:
- The component to be wrapped
- The component that will be shown while the component is loading (the Loader)
- 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.