Circular dependencies between modules are generally a bad thing. They make two modules tightly coupled and there is a very high chance that if you change one of them, that’s going to affect the other. You effectively make two possibly independent modules look like a single unit of code, defeating one of the main purposes of modularisation.
Having said that, despite your good will, you might find yourself in a situation where you need to handle a circular dependency. Depending on which module loader or strategy you are using, adding a circular dependency might work out of the box. Besides, learning how they are handled will give you insights on how your module loader works under the hood.
ES5 – AMD with RequireJS
With RequireJS you need to make some adjustments, as explained in their dedicated API docs chapter: http://requirejs.org/docs/api.html#circular
You can declare the require function as a dependency and use it to fetch the module that you want to load later, rather than as a normal dependency.
Let’s create two modules that depend on each other:
moduleA.js
define(["moduleB"], function(b) {
return {
getThisName : function() {
b.getOtherModuleName();
},
getOtherModuleName : function() {
return b.getThisName();
},
name : "moduleA"
}
});
moduleB.js
define(["moduleA"], function(a) {
return {
getThisName : function() {
return "moduleB";
},
getOtherModuleName : function() {
return a.name;
}
}
});
The first module exposes a couple of functions which delegate the results to the second module and a “name” string, which creates the backward dependency. The latter uses the string exposed by moduleA.
If you import moduleA and print the result of getOtherModuleName it will work, because both modules are loaded at this time. What you cannot do is moduleB.getThisName() because at the time of loading, the moduleA dependency was undefined and it will throw a TypeError.
You can instead load moduleA manually using the require dependency:
moduleB.js
define(["require"], function(require) {
return {
getThisName : function() {
return "moduleB";
},
getOtherModuleName : function() {
return require("moduleA").name;
}
}
});
This is basically moving the dependency on moduleA from load time to run time.
ES5 – CommonJS
Let’s do something similar creating three NodeJS modules:
CommonJS module a
var b = require('./commonjs.moduleB');
module.exports = {
getThisName : function() {
return b.getOtherModuleName();
},
getOtherModuleName : function() {
return b.getThisName();
},
name : "moduleA"
}
CommonJS module b
var a = require('./commonjs.moduleA');
module.exports = {
getThisName : function() {
return "moduleB";
},
getOtherModuleName : function() {
return a.name;
}
}
CommonJS module c
var a = require('./commonjs.moduleA');
console.log(a.getOtherModuleName());
console.log(a.getThisName());
With this scenario the result is similar to the previous AMD example:
$ node commonjs.moduleC.js
moduleB
undefined
This is because, when module B starts, it will initially get an empty object from the module A import, which is nothing but the initial value of the exports object in A. Once A has done loading, it will have exported its own two function overriding that initial reference with a new object literal; that empty object is now disconnected from A. B will try to get the name property from an empty object.
We can fix it by exporting the functions separately without overriding the exports object:
CommonJS module a
var b = require('./commonjs.moduleB');
function getThisName() {
return b.getOtherModuleName();
}
function getOtherModuleName() {
return b.getThisName();
}
exports.getThisName = getThisName;
exports.getOtherModuleName = getOtherModuleName;
exports.name = "moduleA";
CommonJS module B
var a = require('./commonjs.moduleA');
function getThisName() {
return "moduleB";
}
function getOtherModuleName() {
return a.name;
}
exports.getThisName = getThisName;
exports.getOtherModuleName = getOtherModuleName;
If you run it now:
$ node commonjs.moduleC.js
moduleB
moduleA
The variable a in module B will now be properly populated with the exported functions and name once module A finishes loading.
Now let’s play with it a bit and change module A to look like this:
Module A updated
var b = require('./commonjs.moduleB');
function getThisName() {
return b.getOtherModuleName();
}
function getOtherModuleName() {
return b.getThisName();
}
exports.getThisName = getThisName;
exports.getOtherModuleName = getOtherModuleName;
exports.name = "moduleA";
module.exports = {
getThisName : getThisName,
getOtherModuleName : getOtherModuleName,
name : "an alias for A"
}
Bear in mind that exports and module.exports initially point to the same object. If you want to know more about the relationship between the two, have a look at https://nodejs.org/docs/latest/api/modules.html#modules_exports_alias.
Now, if you add a
console.log(a.name)
to module C, you will get a possibly unexpected result:
$ node commonjs.moduleC.js
moduleB
moduleA
an alias for A
This happens because module C is getting the object literal straight away from module A, which completes the loading before the import, while module B is getting the name property from the initial exports object. If that sounds confusing, the best thing you can do is start playing with it a bit or look at the require function source code to understand what’s happening under the hood.
ES 6
With the new ECMAScript standard, a completely different approach has been chosen, which makes circular dependency work without any workaround. The only case in which they won’t work, but that’s valid for the above examples also, is when you try to use the dependency in the module body rather then in a function for later use.
es6 Module a
import * as b from "./moduleB.js";
function getThisName() {
return b.getOtherModuleName();
}
function getOtherModuleName() {
return b.getThisName();
}
var name = "moduleA";
export {
getThisName,
getOtherModuleName,
name
}
es6 Module b
import * as a from "./moduleA";
function getThisName() {
return "moduleB";
}
function getOtherModuleName() {
return a.name;
}
export {
getThisName : getThisName,
getOtherModuleName : getOtherModuleName
}
This will work as expected, like the second CommonJS example. Now, I would love to should you how to run it, but there is no native support for ES6 modules in browsers nor in NodeJS. What you can do is use transpilers like Babel; you’ll see that the code will be translated to something similar to what we had in the second CommonJS example.
The peculiarity of ES6 exports is that they are live and readonly views on the exported values. Take for example the string “moduleA”: you might think that the export happens by value, in which case any subsequent change to the name variable would hold no effect on the exported value; that’s true for CommonJS, but not for ES6 modules.
Let’s make an addition to our Module A:
import * as b from "./moduleB";
function getThisName() {
return b.getOtherModuleName();
}
function getOtherModuleName() {
return b.getThisName();
}
var name = "moduleA";
function updateName(newName) {
name = newName;
}
export {
getThisName,
getOtherModuleName,
updateName,
name
}
We exported another function that updates the name variable. As I said there is no way to see what happens natively, but we can take a look at how Babel interprets it:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.name = exports.updateName = exports.getOtherModuleName = exports.getThisName = undefined;
var _moduleB = require("./moduleB");
var b = _interopRequireWildcard(_moduleB);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function getThisName() {
return b.getOtherModuleName();
}
function getOtherModuleName() {
return b.getThisName();
}
var name = "moduleA";
function updateName(newName) {
exports.name = name = newName;
}
exports.getThisName = getThisName;
exports.getOtherModuleName = getOtherModuleName;
exports.updateName = updateName;
exports.name = name;
This is the Babel output in its “es2015” mode; take a look at what happens now inside the updateName function. It’s not only updating the local variable name, but also the property of the exported object. We saw earlier that exports references the object that will be returned by require, that means that modules depending on module A will see the new name as well.