Lazy loading React Components using RequireJS and Flux

Rolf van de Krol
13 min readAug 30, 2015

When developing large client-side web applications using React, you’ll get to a point where you don’t want to load the whole application at once. Either because the application simply gets to big and you want to cut the loading time, or because you want to load only the parts of the application that a user has access to.

There is a great solution for loading parts of a Javascript application only when you need them: RequireJS. Using RequireJS together with React for lazy loading is a bit tricky, because the loading of RequireJS modules is asynchronous, and the rendering of React components is synchronous. So, let’s first start with how to use React with RequireJS without lazy loading, and show where the problem lies.

The base of a simple application that is build using RequireJS could be written like this:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example app</title>
</head>
<body>
<div id="wrapper"></div>
<script src="js/require.js"></script>
<script>
require.config({
'baseUrl' : 'js/',
});
require(["application"]);
</script>
</body>
</html>

The file js/application.js would be the place where the application is initialized:

define(
[],
function() {
// initialize application
}
);

In the case of a React application, js/application.js would be generated from this JSX file.

define(
['react', 'components/application'],
function(React, Application) {
React.render(
<Application />,
document.getElementById('wrapper')
);
}
);

In all the Javascript examples below, I’ll just write JSX and assume the JSX is converted server-side to Javascript, using Babel, or other tools.

The file js/components/application.js would look like this:

define(
['react'],
function(React) {
return React.createClass({
displayName: 'Application',
render: function() {
return (
<div className="application">
<h1>Example app</h1>
</div>
);
}
});
}
);

As you can see, in the js/application.js example, the Application component is loaded using the dependency management of RequireJS. This would work the same when the Application component uses other components. Of course it will use other components, because we are talking about large applications, where you won’t get away with handling everything in one component.

However, when a component is loaded using the dependency management of RequireJS, the dependency will be loaded before the dependant. And not, based on the React state of the dependant, only when it is needed. In the ideal world, we would be able to write something like the following. Just like how loading a module in Node.js works.

define(
['react'],
function(React) {
return React.createClass({
displayName: 'Application',
render: function() {
if (some condition) {
var Dependency = require('components/dependency');
return (
<div className="application">
<Dependency />
</div>
);
} else {
return (
<div className="application">
<h1>Example app</h1>
</div>
);
}
}
});
}
);

However, that is not how RequireJS works. RequireJS exposes a require method, but this method expects to be used asynchronously, instead of synchronously. To be more precise: it expects to be used asynchronously, the first time a module is loaded. Once the module is loaded, you can also use the synchronous form to access the loaded module. So the first time, the method should be called like this:

require(['components/dependency'], function(Dependency) {
// Do something here with Dependency
});

When the dependency is loaded, you can access the module like this:

var Dependency = require('components/dependency');

So, what we need is a way to load the module before we are going to render it, and let ‘some condition’ be false, until the module is really loaded. This could be implemented by creating a componentDidMount method on the Application component and load the required module there.

define(
['react', 'require'],
function(React, require) {
return React.createClass({
displayName: 'Application',
getInitialState: function() {
return {
dependency_loaded:
require.defined('components/dependency')
};
},
render: function() {
if (this.state.dependency_loaded) {
var Dependency = require('components/dependency');
return (
<div className="application">
<Dependency />
</div>
);
} else {
return (
<div className="application">
<h1>Example app</h1>
</div>
);
}
},
componentDidMount: function() {
var self = this;
if (!this.state.dependency_loaded) {
require(['components/dependency'], function() {
self.setState({dependency_loaded: true});
})
}
}
});
}
);

The example above works just fine, but it has some issues. First, it loads the dependency, irregardless of the state of the application. In a real world application, you would have some UI element that triggers a change in the state and only start loading after the change is triggered. This is of course solvable in this code, but it creates a bit of a spaghetti. Bigger issues are scalability and separation of concerns. This code works just fine with one component, but will grow with every component you add. Even more important: the primary job of the Application component is not to load it’s dependencies. Of course, in this simple example the Application component doesn’t do very much, but in a more useful application the Application component would provide the foundation of the application. And loading dependencies is rarely the foundation of an application.

So, how do we solve these issues, without ending up with a giant ball of intertangled code?

Because React only provides the view part of an application, you need some framework, to structure your application. I’m using Flux in the examples below, but the basic ideas apply to other frameworks as well.

The basic idea of Flux is something called unidirectional data flow, which is nicely summarised by the diagram below.

Unidirectional data flow in Flux. © Facebook, Inc.

Flux is an idea, rather than a framework. The only code it provides is the Dispatcher (and some examples). For the other parts you should use other projects. In my examples, I use React for the views and fbemitter to make the stores expose events. React is quite essential for this story, but the event emitter is just one of the many options. Any other well-written event emitter will do the job, so feel free to use your favourite emitter. The API may differ a little, but that won’t present any problems.

In Flux, everything has it’s designated place, so we can pretty easily make a general layout where which part of the loading of components should be located. The calls to RequireJS to request a component should originate from an Action Creator and result in an Action that gets send into the Dispatcher. The Action indicates that a component is loaded, and should be received by a Store which saves which components are actually loaded. The View queries the Store to figure out if the component is available and listens to events from the Store to update itself when the component’s state is changed.

So, let’s get started with an Application component (js/components/application.js) again:

define(
['react', 'components/plugin'],
function(React) {
return React.createClass({
displayName: 'Application',
render: function() {
if (some condition) {
return (
<div className="application">
<Plugin plugin="components/plugins/foo" />
</div>
);
} else {
return (
<div className="application">
<h1>Example app</h1>
</div>
);
}
}
});
}
);

Now, the Application component doesn’t care if the required component is already loaded, it simply inserts a Plugin component, which will handle the loading of the required component. I choose the word Plugin for the loadable component, but you can call it whatever you want.

Flux prescribes to work with a single Dispatcher, and all Actions should pass through this Dispatcher. If you are already working on a Flux application, you probably already have one, and you should use that Dispatcher. If this is the start of your Flux application, you will need one. So let’s create a js/dispatcher.js file.

define(
['Flux'],
function (Flux) {
var dispatcher = new Flux.Dispatcher();
return dispatcher;
}
);

While developing for Flux, it makes the most sense to first create the four main blocks (Dispatcher, Store, View, Action Creator) and then link them together using the connectors (Callback, Change event, Store query, User interaction, Action). We already have the Dispatcher, so let’s move on to the Store.

The Store is the place where will be saved which Plugins are loaded, and where the Views can retrieve this information. A Store is basically an object with a simple API that has two purposes: event subscription and data retrieval and manipulation. Let’s start with the most simple of those: data retrieval and manipulation.

The Store where the loaded state of Plugins is saved, will be called PluginStore, and is located at js/stores/plugin_store.js. We need an endpoint where we can retrieve whether a plugin is loaded and an endpoint where we can tell the Store that a plugin has been loaded.

define(
[],
function () {
var API = {
loaded: function(name) {},
setLoaded: function(name) {},
};
return API;
}
);

To save this information, a simple Array is enough, so let’s just use that.

var plugins = [];

We mark a Plugin as loaded by adding it to the Array.

plugins.push(name);

We can check if a Plugin is loaded by checking if the Plugin is added to the Array.

plugins.indexOf(name) !== -1

Let’s put it all together to finish the data retrieval and manipulation API.

define(
[],
function () {
var _plugins = [];
var API = {
loaded: function(name) {
return (_plugins.indexOf(name) !== -1);
},
setLoaded: function(name) {
_plugins.push(name);
},
};
return API;
}
);

To indicate that the _plugins variable is internal storage, I added an underscore the the name.

Secondly, a Store should emit events so the View can listen to those. So, there should be an endpoint to add an event listener and an endpoint to remove an event listener.

define(
[],
function () {
var _plugins = [];
var API = {
addLoadListener: function(callback) {},
removeLoadListener: function(callback) {},
loaded: function(name) {
return (_plugins.indexOf(name) !== -1);
},
setLoaded: function(name) {
_plugins.push(name);
},
};
return API;
}
);

We will use fbemitter as the event emitter. The event emitter should be initialized like this:

var events = new fbemitter.EventEmitter();

Adding and removing an event listener is made easy by fbemitter:

events.addListener(event_name, callback);
events.removeListener(event_name, callback);

In order to send meaningful events to the Views, the Store should trigger the fbemitter.

events.emit(event_name);

Gluing these parts together, creates an (almost) finished PluginStore.

define(
['fbemitter'],
function (fbemitter) {
var _plugins = [];
var events = new fbemitter.EventEmitter();
var API = {
addLoadListener: function(callback) {
events.addListener('load', callback);
},
removeLoadListener: function(callback) {
events.removeListener('load', callback);
},
loaded: function(name) {
return (_plugins.indexOf(name) !== -1);
},
setLoaded: function(name) {
_plugins.push(name);
events.emit('load');
},
};
return API;
}
);

What we haven’t implemented yet, is how to link the Store to the Dispatcher. But first we will write the View and the Action Creator.

The View is the Plugin component that was used by the Application component like this:

<Plugin plugin="components/plugins/foo" />

Let’s start with a basic React Plugin component that accepts one property and build it out to make it initialise the lazy loading of components.

define(
['react'],
function(React) {
return React.createClass({
displayName: 'Plugin',
propTypes: {
plugin: React.PropTypes.string.isRequired
},
render: function() {
return (
<div className="plugin">
</div>
);
}
});
}
);

The name of the component that should be loaded can be retrieved from the properties.

this.props.plugin

When the plugin is already loaded, we can display it. Otherwise we need to show a loading message. Whether the plugin is loaded, should be saved in the state. So let’s rewrite the render method to do this. Remember that we can use the synchronous form of require if the component is loaded.

render: function() {
if (this.state.loaded) {
return (
<div className="plugin">
{React.createElement(require(this.props.plugin), null)}
</div>
);
} else {
return (
<div className="plugin-loading">
Loading...
</div>
);
}
}

Of course the value loaded should be put in the state, by loading it from the PluginStore. Because the public API of the PluginStore is already finished, we are going to link the View to the Store. When the Plugin component is mounted, the getInitialState method will be called. In this method, we retrieve the loaded value from the PluginStore.

getInitialState: function() {
return {
loaded: PluginStore.loaded(this.props.plugin)
}
}

When the Plugin is already mounted and receives a new value for the plugin property from the Application, the componentWillReceiveProps method will be called. In our simple example this doesn’t happen, but in a real world application this is very likely. In this method we need to reset the loaded value in the state, because the new plugin doesn’t necessarily have the same loaded state.

componentWillReceiveProps: function(props) {
this.setState({
loaded: PluginStore.loaded(props.plugin)
});
}

Now, we need to link the View to the event emitter of the PluginStore. The plugin may not have to be loaded when the Plugin component is mounted, but the idea is, of course, that it will be loaded at some point.

componentDidMount: function() {
PluginStore.addLoadListener(this._onLoad);
},
componentWillUnmount: function() {
PluginStore.removeLoadListener(this._onLoad);
},
_onLoad: function() {
this.setState({
loaded: PluginStore.loaded(this.props.plugin)
});
}

Now, every time the PluginStore emits a load event, the _onLoad method will be called and this method will reset the loaded state.

The View is now almost finished, so lets put the parts together:

define(
['react', 'stores/plugin_store', 'require'],
function(React, PluginStore, require) {
return React.createClass({
displayName: 'Plugin',
propTypes: {
plugin: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
loaded: PluginStore.loaded(this.props.plugin)
}
},
render: function() {
if (this.state.loaded) {
return (
<div className="plugin">
{React.createElement(
require(this.props.plugin),
null
)}
</div>
);
} else {
return (
<div className="plugin-loading">
Loading...
</div>
);
}
},
componentWillReceiveProps: function(props) {
this.setState({
loaded: PluginStore.loaded(props.plugin)
});
},
componentDidMount: function() {
PluginStore.addLoadListener(this._onLoad);
},
componentWillUnmount: function() {
PluginStore.removeLoadListener(this._onLoad);
},
_onLoad: function() {
this.setState({
loaded: PluginStore.loaded(this.props.plugin)
});
}
});
}
);

All we need to finish the View, is some way to trigger the process of loading a plugin. Because now we are waiting for a process to finish, but the process is never started. This process needs to be handled by an Action Creator, so that’s what we need now.

The Action Creator needs to have an API that allows to trigger the loading of a module. When this process is triggered, the asynchronous form of require needs to be used to load the module, and when it is finished, an Action needs to be dispatched so the fact that the module is loaded can be handled by the PluginStore. If the module is already loaded, the loading process should not start, but the Action should be dispatched immediately. We can check if a module is already loaded by calling the require.defined method.

require.defined(name)

An Action is an object that has a type property and some type-specific properties. In our case, we need to include the name of the loaded plugin in the Action.

{
type: 'plugin-loaded',
plugin: plugin_name
}

The Action is send to the Dispatcher like this:

Dispatcher.dispatch(action);

So, putting these parts together, this is the resulting Action Creator, which needs to be saved in js/actions/plugin_action_creators.js.

define(
['require', 'dispatcher'],
function(require, Dispatcher) {
return {
loadPlugin: function(name) {
if (require.defined(name)) {
Dispatcher.dispatch({
type: 'plugin-loaded',
plugin: name
});
} else {
require([name], function() {
Dispatcher.dispatch({
type: 'plugin-loaded',
plugin: name
});
});
}
},
};
}
);

The Action Creator is the first of the main blocks that is now finished. It creates an Action and sends it to the Dispatcher. Now we need to create the Callbacks that link the Store to the Dispatcher.

The Dispatcher is a bit like an event emitter, but allows only one Action to be dispatched at once and assumes the callbacks that are registered in the Dispatcher to be synchronous. In our case we don’t see much of the synchronous nature of the Dispatcher but in more complicated applications, you will encounter this.

So we need to change the PluginStore to register a Callback in the Dispatcher.

Dispatcher.register(function(action) {
switch(action.type) {
case 'plugin-loaded':
API.setLoaded(action.plugin);
break;
}
});

In the full PluginStore code, this looks like this:

define(
['fbemitter', 'dispatcher'],
function (fbemitter, Dispatcher) {
var _plugins = [];
var events = new fbemitter.EventEmitter();
var API = {
addLoadListener: function(callback) {
events.addListener('load', callback);
},
removeLoadListener: function(callback) {
events.removeListener('load', callback);
},
loaded: function(name) {
return (_plugins.indexOf(name) !== -1);
},
setLoaded: function(name) {
_plugins.push(name);
events.emit('load');
},
};
API.dispatchToken = Dispatcher.register(function(action) {
switch(action.type) {
case 'plugin-loaded':
API.setLoaded(action.plugin);
break;
}
});
return API;
}
);

Note that we save the return value of the register method of the Dispatcher in our API. In our simple case, we don’t need this, but you will need it when you run into the synchronous nature of the Dispatcher. If you want to know more about this, you should read the documentation of the Flux Dispatcher.

Now, when the loadPlugin Action Creator is triggered, the module will be loaded, an Action will be created and dispatched. The PluginStore will receive the Action and update it’s internal storage accordingly and emit a Change event. Now the only part missing is the actual triggering of the Action Creator. This is something that should be done by the View.

We need to add this trigger to the componentDidMount and componentDidUpdate methods. We only need to load the plugin, if it is not already loaded, so we write the trigger like this:

if (!this.state.loaded) {
Actions.loadPlugin(this.props.plugin);
}

We now have all the parts of the final Plugin component, so let’s put those together.

define(
[
'react',
'stores/plugin_store',
'require',
'actions/plugin_action_creators'
],
function(React, PluginStore, require, Actions) {
return React.createClass({
displayName: 'Plugin',
propTypes: {
plugin: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
loaded: PluginStore.loaded(this.props.plugin)
}
},
render: function() {
if (this.state.loaded) {
return (
<div className="plugin">
{React.createElement(
require(this.props.plugin),
null
)}
</div>
);
} else {
return (
<div className="plugin-loading">
Loading...
</div>
);
}
},
componentWillReceiveProps: function(props) {
this.setState({
loaded: PluginStore.loaded(props.plugin)
});
},
componentDidMount: function() {
PluginStore.addLoadListener(this._onLoad);
if (!this.state.loaded) {
Actions.loadPlugin(this.props.plugin);
}
},
componentDidUpdate: function() {
if (!this.state.loaded) {
Actions.loadPlugin(this.props.plugin);
}
},
componentWillUnmount: function() {
PluginStore.removeLoadListener(this._onLoad);
},
_onLoad: function() {
this.setState({
loaded: PluginStore.loaded(this.props.plugin)
});
}
});
}
);

Now, we have completed the Flux diagram. We have created the four main blocks and the connectors, which means we are finished.

In this method of loading the plugin, all the magic happens outside the Application component. This has a lot of practical advantages, because the Application hardly needs to be aware of the fact that the plugin is lazy loaded. If you prefer this, you can also turn this around and let the Application component call the Action Creator after some user interaction and only include the plugin after it has been loaded.

To make this a bit more practical, I’ve created a sample application, which is freely available on Github, and implements both of the loading strategies. The loading strategy that is presented in this article is called the Direct strategy, and the strategy that makes the Application component a bit more aware of the loading is called Async. Futhermore it shows how to make this plugin loading work when you have more than one plugin, because in a practical application of this, you will probably have more than one plugin. Finally it shows how to glue all these parts together using Gulp, gather the required libraries using NPM and how to use the RequireJS optimizer to combine all these little files into larger files to keep the amount of HTTP requests low.

Rolf van de Krol is a Software Engineer at Hoppinger, where he builds complex applications for national and international clients. If you need someone to help you make your online appearance stand out, you should contact Hoppinger.

--

--