Modifying ES6 module exports

# /#!/JoePea (9 years ago)

I'm curious, what should happen when module A imports the default from module B which imports the default from module A? Should the exported A object exist inside of module B?

Here's two modules A and B (simplified from some real code that I have), which have a circular dependency:

// A.js
import B from './B'
let A = window.globalThing
export default A

// modify the A object adding methods that reference B.
// B.js
import A from './A'
let B = new window.OtherThing
export default B

console.log(A) // an empty object, {}

// modify the B object which depends on A existing (not being some temporary
// object like I get with Webpack) while the module is evaluated.

When using Webpack, this code fails because the reference to A in B is some empty object. If I import both A and B into main.js, then A is defined as epected:

// main.js
import A from './A'
import B from './B'

console.log(A) // An object with a bunch of properties, as expected.

I can fix my problem by exporting functions to do setup of A and B, and running those functions in main.js, like this:

// A.js
import B from './B'
let A = window.globalThing
export default A

A.setup = () => {
  // modify the A object adding methods that reference B.
}
// B.js
import A from './A'
let B = new window.OtherThing
export default B

console.log(A) // an empty object, {}

B.setup = () => {
  console.log(A) // the expected object!! Why?

  // modify the B object which depends on A existing (not being some temporary
  // object like I get with Webpack) while the module is evaluated.
}

When using Webpack, this code fails because the reference to A in B is just {}, some empty object. If I import both A and B into main.js, then A is defined as epected:

// main.js
import A from './A'
import B from './B'

A.setup()
B.setup()

// no more problems!

console.log(A) // An object with a bunch of properties, as expected.

So, if I run the setup for A and B in main.js instead of in the module evaluation, everything works perfectly.

This led me to wonder: What is the expected behavior of circular dependencies in ES2015? Have you had this 'empty object' problem before? Is that expected to happen, or might there be a bug in Webpack?

I guess the problem makes sense: How can the first method (running relying on module evaluation for setting up A and B exports) work if module B depends on the evaluation of module A which depends on the evaluation of module B?

# Logan Smyth (9 years ago)

To start, an object is definitely not what I'd expect. The core thing to remember with ES6 modules is that while imports are live bindings, you still need to write your code in such a way that it has no run-time circular dependencies.

In your specific examples, you are importing A first, which will begin B loading. Since A has not finished executing yet, and B will be attempting to access the the A import before the A.js file has begin executing. I'm not 100% sure off the top of my head, but in a real ES6 environment, that would either result in the A import being undefined, or attempting to access it would throw a temporal dead zone error due to you accessing a variable before it has been initialized.

If your A.js file comment "// modify the A object adding methods that reference B." adds those references without actually accessing B in any way, then the solution to your issue (in a real environment anyway) would be to import B.js first so that you do not have circular initialization-time dependencies.

In this case, are you using Babel's ES6 module syntax transpiling, or Webpack with another transpiler?

# /#!/JoePea (9 years ago)

In my case I was using Webpack+babel-loader. I guess that makes sense, about the run-time dependencies. After solving the problem by wrapping the module logic for A and B in functions then calling those functions in main.js. I then encountered another problem: package C imports the default from package B, and at initialization-time B is undefined in C even though there's no circular dependency there. I was able to solve that problem by wrapping init-time logic in a function and again executing that in main.js. So, basically, moving all to calls in main.js solved both of these problems. I can follow what you're saying about the circular dep between A and B, but I'm not at all sure why B would be undefined in C after having moved A and B logic to main.js, leaving C logic still in the C module.

# Ryan Dunckel (9 years ago)
# Caridy Patino (9 years ago)

circular dependencies will work nicely with declarations, not so much with expressions because the code has to be evaluated to set those bindings. This, of course, is not a problem if you're not invoking some initialization when evaluating the body of the module.

all in all, that's what you're experiencing. as for the {}, that's just babel.

side note: this has nothing to do with those being default exports, it will happen for any export.

# Logan Smyth (9 years ago)

The {} shouldn't be from Babel, it has handled all circular dependency cases as far as I'm aware. I'm curious to know what the end cause of this issue is, but this isn't really the right discussion forum for it. Joe, if you do end up making a simplified example that can be tested, I'd be happy to take a look, so feel free to ping me.

# /#!/JoePea (9 years ago)

Cool, thanks guys. Indeed that appears to have been my problem.

# Isiah Meadows (9 years ago)

Actually, Babel defers circular dependencies to the underlying module resolver, so most likely Node/Browserify/etc. is where you're getting that, as that's actually intentional, documented behavior in Node.