EDIT: for more background, also see the discussion on ES Discuss.
I have three modules A
, B
, and C
. A
and B
import the default export from module C
, and module C
imports the default from both A
and B
. However, module C
does not depend on the values imported from A
and B
during module evaluation, only at runtime at some point after all three modules have been evaluated. Modules A
and B
do depend on the value imported from C
during their module evaluation.
The code looks something like this:
// --- Module A
import C from 'C'
class A extends C {
// ...
}
export {A as default}
.
// --- Module B
import C from 'C'
class B extends C {
// ...
}
export {B as default}
.
// --- Module C
import A from 'A'
import B from 'B'
class C {
constructor() {
// this may run later, after all three modules are evaluated, or
// possibly never.
console.log(A)
console.log(B)
}
}
export {C as default}
I have the following entry point:
// --- Entrypoint
import A from './app/A'
console.log('Entrypoint', A)
But, what actually happens is that module B
is evaluated first, and it fails with this error in Chrome (using native ES6 classes, not transpiling):
Uncaught TypeError: Class extends value undefined is not a function or null
What that means is that the value of C
in module B
when module B
is being evaluated is undefined
because module C
has not yet been evaluated.
You should be able to easily reproduce by making those four files, and running the entrypoint file.
My questions are (can I have two concrete questions?): Why is the load order that way? How can the circularly-dependent modules be written so that they will work so that the value of C
when evaluating A
and B
will not be undefined
?
(I would think that the ES6 Module environment may be able to intelligently discover that it will need to execute the body of module C
before it can possibly execute the bodies of modules A
and B
.)
There is another possible solution..
Yes it's a disgusting hack but it works
Here is what I used in my own library:
internal/a.js
internal/b.js
internal/c.js
c.js
a.js
b.js
Entrypoint
I would recommend to use inversion of control. Make your C constructor pure by adding an A and a B parameter like this:
https://www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8u
Update, in response to this comment: How to fix this ES6 module circular dependency?
Alternatively, if you do not want the library consumer to know about various implementations, you can either export another function/class that hides those details:
or use this pattern:
Update, in response to this comment: How to fix this ES6 module circular dependency?
To allow the end-user to import any subset of the classes, just make a lib.js file exporting the public facing api:
or:
Then you can:
The answer is to use "init functions". For reference, look at the two messages starting here: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21
The solution looks like this:
-
-
-
Also see this thread for related info: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688
It is important to note that exports are hoisted (it may be strange, you can ask in esdiscuss to learn more) just like
var
, but the hoisting happens across modules. Classes cannot be hoisted, but functions can be (just like they are in normal pre-ES6 scopes, but across modules because exports are live bindings that reach into other modules possibly before they are evaluated, almost as if there is a scope that encompasses all modules where identifiers can be accessed only through the use ofimport
).In this example, the entry point imports from module
A
, which imports from moduleC
, which imports from moduleB
. This means moduleB
will be evaluated before moduleC
, but due to the fact that the exportedinitC
function from moduleC
is hoisted, moduleB
will be given a reference to this hoistedinitC
function, and therefore moduleB
call callinitC
before moduleC
is evaluated.This causes the
var C
variable of moduleC
to become defined prior to theclass B extends C
definition. Magic!It is important to note that module
C
must usevar C
, notconst
orlet
, otherwise a temporal deadzone error should theoretically be thrown in a true ES6 environment. For example, if module C looked likethen as soon as module
B
callsinitC
, an error will be thrown, and the module evaluation will fail.var
is hoisted within the scope of moduleC
, so it is available for wheninitC
is called. This is a great example of a reason why you'd actually want to usevar
instead oflet
orconst
in an ES6+ environment.However, you can take note rollup doesn't handle this correctly https://github.com/rollup/rollup/issues/845, and a hack that looks like
let C = C
can be used in some environments like pointed out in the above link to the Meteor issue.One last important thing to note is the difference between
export default C
andexport {C as default}
. The first version does not export theC
variable from moduleC
as a live binding, but by value. So, whenexport default C
is used, the value ofvar C
isundefined
and will be assigned onto a new variablevar default
that is hidden inside the ES6 module scope, and due to the fact thatC
is assigned ontodefault
(as invar default = C
by value, then whenever the default export of moduleC
is accessed by another module (for example moduleB
) the other module will be reaching into moduleC
and accessing the value of thedefault
variable which is always going to beundefined
. So if moduleC
usesexport default C
, then even if moduleB
callsinitC
(which does change the values of moduleC
's internalC
variable), moduleB
won't actually be accessing that internalC
variable, it will be accessing thedefault
variable, which is stillundefined
.However, when module
C
uses the formexport {C as default}
, the ES6 module system uses theC
variable as the default exported variable rather than making a new internaldefault
variable. This means that theC
variable is a live binding. Any time a module depending on moduleC
is evaluated, it will be given the moduleC
's internalC
variable at that given moment, not by value, but almost like handing over the variable to the other module. So, when moduleB
callsinitC
, moduleC
's internalC
variable gets modified, and moduleB
is able to use it because it has a reference to the same variable (even if the local identifier is different)! Basically, any time during module evaluation, when a module will use the identifier that it imported from another module, the module system reaches into the other module and gets the value at that moment in time.I bet most people won't know the difference between
export default C
andexport {C as default}
, and in many cases they won't need to, but it is important to know the difference when using "live bindings" across modules with "init functions" in order to solve circular dependencies, among other things where live bindings can be useful. Not to delve too far off topic, but if you have a singleton, alive bindings can be used as a way to make a module scope be the singleton object, and live bindings the way in which things from the singleton are accessed.One way to describe what is happening with the live bindings is to write javascript that would behave similar to the above module example. Here's what modules
B
andC
might look like in a way that describes the "live bindings":This shows effectively what is happening in in the ES6 module version: B is evaluated first, but
var C
andfunction initC
are hoisted across the modules, so moduleB
is able to callinitC
and then useC
right away, beforevar C
andfunction initC
are encountered in the evaluated code.Of course, it gets more complicated when modules use differing identifiers, for example if module
B
hasimport Blah from './c'
, thenBlah
will still be a live binding to theC
variable of moduleC
, but this is not very easy to describe using normal variable hoisting as in the previous example, and in fact Rollup isn't always handling it properly.Suppose for example we have module
B
as the following and modulesA
andC
are the same:Then if we use plain JavaScript to describe only what happens with modules
B
andC
, the result would be like this:Another thing to note is that module
C
also has theinitC
function call. This is just in case moduleC
is ever evaluated first, it won't hurt to initialize it then.And the last thing to note is that in these example, modules
A
andB
depend onC
at module evaluation time, not at runtime. When modulesA
andB
are evaluated, then require for theC
export to be defined. However, when moduleC
is evaluated, it does not depend onA
andB
imports being defined. ModuleC
will only need to useA
andB
at runtime in the future, after all modules are evaluated, for example when the entry point runsnew A()
which will run theC
constructor. It is for this reason that moduleC
does not needinitA
orinitB
functions.It is possible that more than one module in a circular dependency need to depend on each other, and in this case a more complex "init function" solution is needed. For example, suppose module
C
wants toconsole.log(A)
during module evaluation time beforeclass C
is defined:Due to the fact that the entry point in the top example imports
A
, theC
module will be evaluated before theA
module. This means thatconsole.log(A)
statement at the top of moduleC
will logundefined
becauseclass A
hasn't been defined yet.Finally, to make the new example work so that it logs
class A
instead ofundefined
, the whole example becomes even more complicated (I've left out module B and the entry point, since those don't change):-
Now, if module
B
wanted to useA
during evaluation time, things would get even more complicated, but I leave that solution for you to imagine...