Circular imports


#1

I have two modules.

moduleA.js:

import {User} from './moduleB';

export class Base {
  static foo() {
    User.doSomething()
  }
}

moduleB.js:

import {Base} from './moduleA';

class User extends Base {
  static doSomething() {
    // ...
  }
}

The issue is that this does not work because of the circle imports. I get Class extends value undefined is not a constructor or null error. In Python, I would simple do:

export class Base {
  static foo() {
    import {User} from './moduleB';
    User.doSomething()
  }
}

But here it does not work. I got it to work by doing;

export class Base {}

(function () {
  import {User} from './moduleB';

  Base.foo = function foo() {
    User.doSomething();
  };
})();

But I do not really understand why this works. And if this is really the best one can do.


#2

When you import moduleA in moduleB, moduleA isn’t evaluated yet (it “paused” at import { User } from "./moduleB";), so Base is still undefined. In your last example, moduleA can be evaluated completely before moduleB is imported, which means that Base isn’t undefined in moduleB (because you don’ t depend on moduleB to define Base). It’s similar to what you did in the third code block. Why doesn’t it work if you import moduleB in foo?


#3

In my experience, when you encounter these issues it’s not a code problem, but a problem of abstraction and responsibility. You are giving classes too much responsibility, beyond what they should have access to for the sake of convenience. The end result is an intertwined web of dependencies with few clear boundaries and fuzzy methods. Like calling Food.getPeopleEatingPizza, you make the Food class also responsible for finding people.

This can be solved by going “one level up”, having files/classes responsible for the intersection of multiple domains, that import both classes and does meaningful things to them. Preferably, you have very simple data models, that can be easily imported by such classes that transform them to the different needs you might have, instead of having “fat” models stirring all kind of different pots.

If you have a class called “Base”, you’d really expect it to form the “base” of other things to build upon. If your base starts reaching up and grabbing things it should not know about, it’s no longer a base.


#4

I’ve actually had a similar situation recently with my socialize:commentable package when trying to port it to NPM for React Native. Ultimately I decided that the cleanest solution was to put the dependent classes in the same file and export them both from there


#5

Yea. In some pure OOP world maybe. To me it is simply “a bunch of logic all other classes should have”. And then that logic also has something where you need other logic. In my case for example, Base is something which all documents should have, and users are documents. But also all documents should have permission checks. And you need users for that. And you have a loop. :slight_smile:


#6

I do not know why it does not. I would assume it just would. But when calling foo I get that User is not defined.


#7

Okay, so you have documents, which have Base functionality, but need User checks. That’s where the example given seems a bit misleading, as you are calling static methods without any arguments. If it was just that, you would just need to re-structure the hierarchy as I suggested.

But if you are modifying instance variables, or passing them, then you’d obviously need to do something else. Say you took the user doc as an argument to the validate method, and preformed static checks on that user object? Would that work? Then you would not need to import the user in Base, you could for example import a module like Auth and do Auth.userCanModify(user, doc) or something.

It all depends on how you are using the docs I guess, do you have an example which is more in line with the usage?


#8

For real examples, see code here and here.

But yea, I just like it this way. Now the question is how to make it work and why the following does not work:

export class Base {
  static foo() {
    import {User} from './moduleB';
    User.doSomething()
  }
}

#9

Another alternative to this is to have another file that imports Base and User, and defines the functions there, as you can be sure both modules have been resolved. so a file like “base_static_methods” that’s just imported as a part of the app, containing

import {User} from './moduleB';
import {Base} from './moduleA';

Base.foo = function foo() {
  User.doSomething();
};

Base.bar = function bar() {
  User.doSomethingElse();
};

This was an approach I took in my app at some point, and regretted it later as I was giving away too much responsibility to this class. I later abstracted this functionality to create fewer dependencies.


#10

So looking at the methods being used, you have just 1 static method in User that you need, hasPermission.
So, could you move this to a “permission.js” living alongside base.js, which had only this static method, and call it like Permission.userHasPermission(permssion, user) ?
that way, both User and Document could import it, and you woud not need to import User from Document, and you could move the static functions in Document back into the class.


#11

Another way would be moving the static hasPermission from User to Base. That would solve it, right? As the static hasPermission uses nothing in User anyway, it just inspects the user object provided.


#12

But only user documents should have that method, not all. :wink:

Thank you all for suggestions. I will try to think about this a bit more.

I do have some questions about internals. So how is the following done:

How it is “paused”?

And importing inside the method does not work and it complains that User is not defined?


#13

Oh, in fact it works! I figured it out. Because I was copy-pasting around, WebStorm automatically added import on top as well and that one was failing. But undo also removed it so I didn’t notice. So yea, just having imports inside methods works. So cool.


#14

But it is static, so the document instances don’t really have it… And since all docs have canUser and restrictQuery, which both use hasPermission, all documents are using it by proxy anyway. I don’t mean to be argumentative, I guess I’m just not seeing the whole and the reason for having that method in User.

Anyway, happy you got the import working!


#15

This is the execution path I tried to explain. :smile:


#16

Oh, this is so just because the export class Base {} already finishes and so it gets defined.