How to handle circular dependencies with RequireJS/AMD?
Asked Answered
A

7

80

In my system, I have a number of "classes" loaded in the browser each a separate files during development, and concatenated together for production. As they are loaded, they initialize a property on a global object, here G, as in this example:

var G = {};

G.Employee = function(name) {
    this.name = name;
    this.company = new G.Company(name + "'s own company");
};

G.Company = function(name) {
    this.name = name;
    this.employees = [];
};
G.Company.prototype.addEmployee = function(name) {
    var employee = new G.Employee(name);
    this.employees.push(employee);
    employee.company = this;
};

var john = new G.Employee("John");
var bigCorp = new G.Company("Big Corp");
bigCorp.addEmployee("Mary");

Instead of using my own global object, I am considering to make each class its own AMD module, based on James Burke's suggestion:

define("Employee", ["Company"], function(Company) {
    return function (name) {
        this.name = name;
        this.company = new Company(name + "'s own company");
    };
});
define("Company", ["Employee"], function(Employee) {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };
    return Company;
});
define("main", ["Employee", "Company"], function (Employee, Company) {
    var john = new Employee("John");
    var bigCorp = new Company("Big Corp");
    bigCorp.addEmployee("Mary");
});

The issue is that before, there was no declare-time dependency between Employee and Company: you could put the declaration in whatever order you wanted, but now, using RequireJS, this introduces a dependency, which is here (intentionally) circular, so the above code fails. Of course, in addEmployee(), adding a first line var Employee = require("Employee"); would make it work, but I see this solution as inferior to not using RequireJS/AMD as it requires me, the developer, to be aware of this newly created circular dependency and do something about it.

Is there a better way to solve this problem with RequireJS/AMD, or am I using RequireJS/AMD for something it was not designed for?

Albertson answered 2/2, 2011 at 23:12 Comment(0)
L
59

This is indeed a restriction in the AMD format. You could use exports, and that problem goes away. I find exports to be ugly, but it is how regular CommonJS modules solve the problem:

define("Employee", ["exports", "Company"], function(exports, Company) {
    function Employee(name) {
        this.name = name;
        this.company = new Company.Company(name + "'s own company");
    };
    exports.Employee = Employee;
});
define("Company", ["exports", "Employee"], function(exports, Employee) {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee.Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };
    exports.Company = Company;
});

Otherwise, the require("Employee") you mention in your message would work too.

In general with modules you need to be more aware of circular dependencies, AMD or not. Even in plain JavaScript, you have to be sure to use an object like the G object in your example.

Lalia answered 3/2, 2011 at 0:17 Comment(5)
I thought you had to declare exports in both callbacks' argument list, like function(exports, Company) and function(exports, Employee). Anyway, thanks for RequireJS, it's awsome.Aranda
@Lalia I think this can be done one-directionally right, for a mediator or core or other top-down component? Is this a terrible idea, to make it accessible using both methods? #11265327Bly
I'm not sure I understand how this solves the problem. My understanding is that all the dependencies must be loaded before the define runs. Is that not the case if "exports" is passed as the first dependency?Boogeyman
don't you miss exports as param in function?Sydelle
To follow up on @shabunc's point about the missing export param, see this question: #28193882Flexible
W
16

I think this is quite a drawback in larger projects where (multi-level) circular dependencies dwell undetected. However, with madge you can print a list of circular dependencies to approach them.

madge --circular --format amd /path/src
Whipperin answered 19/10, 2013 at 18:10 Comment(1)
CACSVML-13295:sc-admin-ui-express amills001c$ madge --circular --format amd ./ No circular dependencies found!Messick
R
9

If you don't need your dependencies to be loaded at the start (e.g., when you are extending a class), then this is what you can do: (taken from http://requirejs.org/docs/api.html#circular)

In the file a.js:

    define( [ 'B' ], function( B ){

        // Just an example
        return B.extend({
            // ...
        })

    });

And in the other file b.js:

    define( [ ], function( ){ // Note that A is not listed

        var a;
        require(['A'], function( A ){
            a = new A();
        });

        return function(){
            functionThatDependsOnA: function(){
                // Note that 'a' is not used until here
                a.doStuff();
            }
        };

    });

In the OP's example, this is how it would change:

    define("Employee", [], function() {

        var Company;
        require(["Company"], function( C ){
            // Delayed loading
            Company = C;
        });

        return function (name) {
            this.name = name;
            this.company = new Company(name + "'s own company");
        };
    });

    define("Company", ["Employee"], function(Employee) {
        function Company(name) {
            this.name = name;
            this.employees = [];
        };
        Company.prototype.addEmployee = function(name) {
            var employee = new Employee(name);
            this.employees.push(employee);
            employee.company = this;
        };
        return Company;
    });

    define("main", ["Employee", "Company"], function (Employee, Company) {
        var john = new Employee("John");
        var bigCorp = new Company("Big Corp");
        bigCorp.addEmployee("Mary");
    });
Rosenstein answered 22/10, 2013 at 23:46 Comment(1)
As Gili said in his comment, this solution is wrong and will not always work. There is a race condition on which code block will be executed first.Elnoraelnore
T
7

I looked at the docs on circular dependencies :http://requirejs.org/docs/api.html#circular

If there is a circular dependency with a and b , it says in your module to add require as a dependency in your module like so :

define(["require", "a"],function(require, a) { ....

then when you need "a" just call "a" like so:

return function(title) {
        return require("a").doSomething();
    }

This worked for me

Tysontyumen answered 25/6, 2015 at 21:17 Comment(0)
D
5

I would just avoid the circular dependency. Maybe something like:

G.Company.prototype.addEmployee = function(employee) {
    this.employees.push(employee);
    employee.company = this;
};

var mary = new G.Employee("Mary");
var bigCorp = new G.Company("Big Corp");
bigCorp.addEmployee(mary);

I don't think it's a good idea to work around this issue and try to keep the circular dependency. Just feels like general bad practice. In this case it can work because you really require those modules for when the exported function is called. But imagine the case where modules are required and used in the actual definition functions itself. No workaround will make that work. That's probably why require.js fails fast on circular dependency detection in the dependencies of the definition function.

If you really have to add a work around, the cleaner one IMO is to require a dependency just in time (in your exported functions in this case), then the definition functions will run fine. But even cleaner IMO is just to avoid circular dependencies altogether, which feels really easy to do in your case.

Detonator answered 6/8, 2014 at 21:9 Comment(1)
You are suggesting to simplify a domain model and make it less usable just because the requirejs tool does not support that. Tools are supposed to make developer's life easier. The domain model is pretty simple - employee and company. The employee object should know what company(s) he works for, companies should have a list of employees. The domain model is right, it's the tool that fails hereRinglet
S
5

All the posted answers (except https://mcmap.net/q/259805/-how-to-handle-circular-dependencies-with-requirejs-amd) are wrong. Even the official documentation (as of November 2014) is wrong.

The only solution that worked for me is to declare a "gatekeeper" file, and have it define any method that depends on the circular dependencies. See https://mcmap.net/q/260988/-how-to-handle-circular-dependencies-duplicate for a concrete example.


Here is why the above solutions will not work.

  1. You cannot:
var a;
require(['A'], function( A ){
     a = new A();
});

and then use a later on, because there is no guarantee that this code block will get executed before the code block that uses a. (This solution is misleading because it works 90% of the time)

  1. I see no reason to believe that exports is not vulnerable to the same race condition.

the solution to this is:

//module A

    define(['B'], function(b){

       function A(b){ console.log(b)}

       return new A(b); //OK as is

    });


//module B

    define(['A'], function(a){

         function B(a){}

         return new B(a);  //wait...we can't do this! RequireJS will throw an error if we do this.

    });


//module B, new and improved
    define(function(){

         function B(a){}

       return function(a){   //return a function which won't immediately execute
              return new B(a);
        }

    });

now we can use these modules A and B in module C

//module C
    define(['A','B'], function(a,b){

        var c = b(a);  //executes synchronously (no race conditions) in other words, a is definitely defined before being passed to b

    });
Shere answered 27/11, 2014 at 20:48 Comment(3)
btw, if you are still having trouble with this, @yeahdixon's answer should be correct, and I think the documentation itself is correct.Messick
I agree that your methodology works but I think the documentation is correct, and might be one step closer to "synchronous".Messick
you can because all the variables are set at load. Unless your users are time travelers and click the button before it exists. It will break causality and then a race condition is possible.Bestial
F
0

In my case I solved the circular dependency by moving the code of the "simpler" object into the more complex one. For me that was a collection and a model class. I guess in your case I would add the Employee-specific parts of Company into the Employee class.

define("Employee", ["Company"], function(Company) {
    function Employee (name) {
        this.name = name;
        this.company = new Company(name + "'s own company");
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };

    return Employee;
});
define("Company", [], function() {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    return Company;
});
define("main", ["Employee", "Company"], function (Employee, Company) {
    var john = new Employee("John");
    var bigCorp = new Company("Big Corp");
    bigCorp.addEmployee("Mary");
});

A bit hacky, but it should work for simple cases. And if you refactor addEmployee to take an Employee as parameter, the dependency should be even more obvious to outsiders.

Flemish answered 5/9, 2018 at 8:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.