ES6 Module Gotchas

Updated for 2018

Now that ES6 has a finalized module definition (Right now still in draft phase Out of draft phase! And some new ones coming.), I’ve gone through and found some of the things that stick out to me as ‘need to knows’. It’s helpful to keep these few things in mind when working with the new module syntax.

ES6 modules export bindings, not values.
Exporting any primitive will be a ‘live’ value that can and will be changed by the imported module. This is much different than CommonJS or AMD behaviors.

1
2
3
4
5
6
7
8
9
10
11
// library.js
export let counter = 1;
export function increment() {
counter++;
}

// program.js
import { increment, counter } from 'library';
console.log(counter); // 1
increment();
console.log(counter); // 2 !!!

The import of the integer isn’t a pass-by-value situation. You are actually getting a binding to the integer itself; always be aware of this as it will cause many problems when refactoring old code or in new usage as it changes how the logic works. You can read some of the discussion on this topic here.

Returning an object from a module is an anti-pattern.
ES6 modules were designed with a static module structure preferred. This allows the code to be able to discover the import/export values at compile time rather than runtime. Exporting an object from a module allows for unexpected situations and removes the static compilation benefits. Take for instance this code:

1
2
3
4
5
6
7
8
9
10
11
12
13
 // module.1.js
export var myfunc = function() {...}
export default { afunction: myfunct }

// module.2.js
import funcs from 'module.1.js';
funcs.anotherfunc = function() {...};

export default funcs;

// module.3.js
import funcs from 'module.1.js';
funcs.anotherfunc(); // works? yep.

The default export in this case is an object which is actually a binding, not a value. That a means that after exporting the object, different functions can be added or removed from this export, which will update the actual exported module. However, there is no guarantee that this module used in another manner will output the same added functions since you may not always require the second module from importing. This non-static style exporting is typical in a CommonJS codebase (even NodeJS exports the fs object as default like this) but it begins to break down in ES2015+ modules, especially while it’s required to transpile (Traceur or ES6-module-transpiler for instance) to use them today.
You can however get the same effect by using named exports and statically importing them. This is a much more useful way of handling object exporting and importing (And also useful to see how the AirBnB lint standards deal with this)

1
2
3
import * as _ from 'underscore';

_.each( x =< console.log(x) );

It’s important to note that one thing that modern libraries have done to avoid entire imports is to create smaller libraries that can be imported separately for each method. This is highly advantageos, especially for ‘tree shaking’ scenarios.

1
2
import _ from 'lodash'; // NO
import get from 'lodash/get'; // YES!

If you will have side-effects, separate them and load them in a module with short syntax.
The standard import looks something like “import something from ‘somewhere/else’;”. But what if the module you are importing isn’t actually exporting anything and only used to run code. As you move into modules, you will find at first side-effects are going to happen. For example.

1
2
3
// ... code
window.myLib = lib;
// ^ side effect occurs when you import this module!

The only alternative is to separate this code into it’s own module.

1
2
3
4
5
6
7
8
// sideeffects.js
window.myglob = { ... }
window.myglobfunc = function() { ... };
export default null;

//init.js
import sideeffects from 'sideeffects.js';
import moresideeffects form 'moresideeffects.js'

But now you are having to create variables on the import statements; that is not pretty or maintainable. ES6 module syntax has a much better way of doing these imports that aren’t actually setting variables to anything. Basically, import the file without requesting the exports.

1
2
3
// betterinit.js
import 'sideeffects.js'
import 'moresideeffects.js'

Dropping the variable from allows to import side-effects without the need to make up variable names that are equal to null.

Attempt to use import default at all times.
Named exports are fine to use and part of the spec, but defaults are preferred and your code will flow better. It will encourage smaller modules that do less and will help keep your code a bit easier to test.
ES6 modules prefer default exports. This was by design. It becomes a code-smell if your files begin to look like bracket central.

1
2
3
// codesmells.js
import { namedvar1, namedvar2, namedvar3, namedvar4 } from 'poordesignedmodule'
import { anothervar, twovars, orthreevars } from 'anotherpoordesign'

After a dozen of these at the top of a single file, it should become very apparent that your modules are filled with too many functions and are not properly breaking down into smaller modules. It’s not bad to use named imports, but it’s a clear indicator that if all imports are using named imports, modules are doing too much and you risk having more bugs and complexity in them. It’s a good thing to keep in mind as a leading flag of a need to refactor.

Avoid extra syntax if exporting from imports.
It’s really simple to fall into this trap.

1
2
3
4
import something from 'somewhere/else';
// ... code
var mysomething = something;
export mysomething;

It might look ridiculous, but as you get into larger files, you may forget what’s what and where it’s coming from. You can avoid this by exporting directly from the other file.

1
export something from 'somewhere/else';

Some of these are tips, some are tricks; all of them feel new and arguably different to me and it’s good to be aware of them.