Using Scope to Resolve Circular Dependency/Dynamic Loading Issues in ES6-webpack

Rohan Sahai
4 min readDec 13, 2016

Back Story:

I recently took on the task of transforming a codebase I was working in over to ECMAScript 6 — the latest version of javascript. Prior to ES6, we were using requirejs to load up our various js modules (AMD), but now that we could use the built in import and export directives that ship with ES6 (in combination with webpack) we no longer needed to use require.

Below is a visual before and after of a made up js module from our codebase.

Before:

define([
'models/BaseModel'
], function(BaseModel){

var AssetModel = BaseModel.extend({

urlRoot: function(){
return '/api/assets/';
},
associations: {
'comments': {
'path': 'collections/CommentCollection',
'many': true,
}
},
someInstanceMethod: function(){
return 'This will do fancy things in the future';
},
...

});

return AssetModel;
});

After:

import BaseModel from 'models/BaseModel';

let AssetModel = BaseModel.extend({

urlRoot() {
return '/api/assets/';
},
associations: {
'comments': {
'path': 'collections/CommentCollection',
'many': true,
}
},
someInstanceMethod(){
return 'This will do fancy things in the future';
},
});

export default AssetModel;

The Problem:

There were a few different areas in the codebase in which we were loading modules dynamically (sort of). With out going into too much detail — one example was in our BaseModel , which all of our frontend models extend. If a given model had associations defined, and it’s related model/collection was requested, the base model would require that module on the fly. Take a look at the associations blob on the above models and then take a look at the code below for a more clear example:

// some module that has an asset model wants access to it's comments
var asset_comments = asset_model.get('comments');
// BaseModel get method
get: function(attr){
if (model.associations.hasOwnProperty(attr)){
// load up the associated module and return that
var file_path_of_associated_module =
model.associations[attr][path];
return require(file_path_of_associated_module);
}
}

Notice the above require call. This was a pretty neat solution to abstracting association logic into a base model that all models could extend. The reason this worked synchronously, however, is because we defined all our models as a dependency to our core application so they are actually actually loaded right off the bat. We did this by having a module solely dedicated to defining the models/collections that could be required ‘on the fly’. I.E:

// load-models-and-collections.js
define([
'models/AssetModel',
'models/CommentModel',
'collections/AssetCollection',
'collections/CommentCollection',
...
], function() {
});// entry point to our frontend application
define([
'load-models-and-collections.js'
], function() {
startApplication();
});

Since our entry point application required the load-models-and-collection.js as a dependency, all the potential models and collections that could be loaded up as dependencies don’t need to be re-loaded and can be loaded synchronously when called in a future require.

This solution worked well for us but when switching over to es6/webpack and away from require, we ran into some issues. Mainly, when trying to mimic the load-models-and-collection file so that we could dynamically load modules, we either didn’t have access to the modules we wanted in BaseModel or we had circular dependencies that didn’t occur using the AMD route. The reason being, require creates one large dependency tree and stores each of those objects globally whereas webpack loads only “what” you need and “when” you need (https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9#.3tselzvoo). More specifically, webpack scans all the ‘requires’ at compilation time so the dynamic require can’t work synchronously.

In regards to our application this means we don’t have access to some potential association likeAssetModel from BaseModel even if it’s loaded previously from a separate file like the entry point of the application. Furthermore, if we try and require AssetModel in BaseModel we have a circular dependency because AssetModel requires BaseModel (it extends it).

The Solution:

I’m sure there are some webpack intricacies one could tap into to solve the issue, but the cleanest way we found was simply creating a new scope for all of our modules that need to be loaded up on the fly. The easiest way to demonstrate is with code, see below.

// AssociationsScope.js
export default ({}); ** JUST AN EMPTY OBJECT **
// models/AssetModel.js
import BaseModel from 'models/BaseModel';
import AssociationScope from 'AssociationScope';
** ADD THE MODEL TO THE SCOPE **
AssociationScope.AssetModel = BaseModel.extend({

urlRoot() {
return '/api/assets/';
},
associations: {
'comments': {
'path': 'collections/CommentCollection',
'many': true,
}
},
someInstanceMethod(){
return 'This will do fancy things in the future';
},
});

export default AssociationScope.AssetModel;
// load-models-and-collections.js
** LOAD MODEL AND COLLECTIONS SO THEY GET ADDED TO SCOPE **
import 'models/AssetModel',
import 'models/CommentModel',
import 'collections/AssetCollection',
import 'collections/CommentCollection',
...
// BaseModel.js
import AssociationScope;
** Access all models and collections without circular dependency! **
get: function(attr){
if (model.associations.hasOwnProperty(attr)){
// load up the associated module and return that
let association = model.associations[attr][path];
return AssociationScope[association];
}
}
// entry point to our frontend application
** Make sure this is called before any 'import models/BaseModel'! **
import 'load-models-and-collections.js';startApplication();

Note AssociationScope which is just defined as an empty object. Since we load all of our models/collections into the scope we can simply import this scope in our BaseModel and we 1- avoid circular dependencies 2- have access to all the models and collections we need since they are loaded into the scope from the entry point of our application.

--

--