ES6: Use of "import { property } from 'module'" is Not a Great Plan
The Javascript ES6 standard brings a new syntax for declaring imports from other modules and exports from the present modules. Here I'll argue that some of that syntax, specifically the ability to import one or more named properties from a module, is overused in the community and causes more trouble than it is worth when writing ES6 Javascript applications. The ES6 syntax for import and export is noted below in brief:
// Import a module without any import bindings, just to // execute its code without assigning any variables here. import 'example'; // Import the default export of a module. import exampleDefaultExport from 'example'; // Import a named export of a module. import { property } from 'example'; // Import a named export to a different name, import { property as exampleProperty } from 'example'; // Import all exports from a module as properties of an object. import * as example from 'example'; // Export a named variable. export var property = 'example property'; // Export a named function. export function property() {}; // Export an entity to the default export. export default 'example default'; // Export an existing variable. var property = 'example property'; export { property }; // Export an existing variable as a new name. export { property as exampleProperty }; // Export an export from another module. export { property as exampleProperty } from 'example'; // Export all exports from another module. export * from 'example';
Import and Export in Node.js and ES5
There are both obvious and subtle differences between the ES6 syntax listed above and the module frameworks in common use for ES5 Javascript ecosystems, such as Node.js. In the Node.js ES5 world, every module exports a default object unless that is replaced, and an entirely empty module still exports an object with no properties. There is only one way to import a module, and that imports either the default export object, or whatever that object was explicitly replaced with. So:
exampleObject.js:
// Exports { x: 'y' }. exports.x = 'y'; // This has the same result; exports is just a convenient reference // to module.exports: // module.exports.x = 'y';
exampleString.js:
// Overwrite the default exports object to export 'y'. module.exports = 'y';
exampleEmptyModule.js:
// There is no code in this module.
exampleImports.js:
// This is { x: 'y' }. var exampleObject = require('./exampleObject'); // This is 'y'. var exampleString = require('./exampleString'); // This is {}. var exampleEmptyModule = require('./exampleEmptyModule');
Import and Export in ES6
In ES6 modules there is no one straightforward analogy to module.exports in Node.js ES5, though it can be recreated by some combinations of export and import definitions:
example.es6.js:
// Set the default exported property to { x: 'y' }. export default { x: 'y' };
exampleProperty.es6.js:
// Set the exported property x to 'y'. export var x = 'y';
exampleEmptyModule.es6.js:
// There is no code in this module.
exampleImports.es6.js:
// This default import obtains an object in the same way as ES5 // require('./example'), but requires that the object was // explicitly exported as the default. // // This is { x: 'y' }. import exampleDefault from './example'; // We can see the default as a property on the object returned by // this form of import. // // This is something like: { default: { x: 'y' } }. import * as example from './example'; // Things get more interesting without the default export. // // This is 'y'. import { x } from './exampleProperty'; // This is undefined - it is the default export and this module // has no default export, only exported properties. import examplePropertyDefault from './exampleProperty'; // This is an object, however, in much the same way as ES5 // require('./exampleProperty'), but again requires that the // export was set up a certain way. // // This is { x: 'y' }. import * as exampleProperty from './exampleProperty'; // For an empty module: // // This, the default export, is undefined. import exampleEmptyModuleDefault from './exampleEmptyModule'; // This is {}. import * as exampleEmptyModule from './exampleEmptyModule';
The Syntax "import { x } from 'y'" is Popular
If you look at the ES6 ecosystem, you'll see that the use of property import is popular. I'm seeing a lot of it now that I'm working with modules relating to React, both in documentation and code. In Redux, for example, important functions are exported as properties in modules with no default export, and the documentation provides examples like the following:
import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp)
This could equally be written in the following way:
import * as redux from 'redux' import todoApp from './reducers' let store = redux.createStore(todoApp)
It isn't, however, and it doesn't take very much work on a real application to start to see that using property import is a problem. Why is it a problem? Because it impedes the use of mocking and stubbing in tests, such as the functionality provided by frameworks like Sinon. It is somewhere between very hard and impossible to write sufficient unit tests without the ability to stub and spy on function calls. It requires an annoying amount of additional boilerplate code to make that possible for property importing, as the following small examples illustrate.
Stubbing in ES5 Javascript Testing
Stubbing a function in Javascript requires the function to be bound to a context, any context, that is in scope for both the test code and the code being tested. In a sane world this context is provided by the module. For example, in ES5 Node.js:
lib/example.js
exports.fn = function () {};
lib/invokeExample.js
var example = require('./example'); // Usage. exports.invokeExample = function () { return example.fn(); };
test/lib/invokeExample.spec.js
var sinon = require('sinon'); var example = require('./example'); var invokeExample = require('../../lib/invokeExample'); // The example function can be stubbed directly, and the // stub will now be used when invokeExample.invokeExample // is called. sinon.stub(example, 'fn').return({});
The thing to avoid doing in ES5 is the following, overriding module.exports with a function. All too many people do this and it is inconsiderate, as any module using that code must then take extra steps to be usefully unit tested:
lib/example.js
module.exports = function () {};
lib/invokeExample.js
var example = require('./example'); // Exported for test purposes. If we don't do this, then // example is encapsulated here and cannot be stubbed. exports.example = example; // Usage. exports.invokeExample = function () { return exports.example(); };
test/lib/invokeExample.spec.js
var sinon = require('sinon'); var invokeExample = require('../../lib/invokeExample'); // The example function can only be stubbed because additional code was // written to export it and use the export explicitly inside invokeExample. sinon.stub(invokeExample, 'example').return({});
Stubbing in ES6
In ES6 using "import { x } from 'y'" is analogous to overwriting module.exports with a function in ES5. The result is that an imported function is encapsulated in the module and cannot be stubbed in unit tests without writing more boilerplate code that would otherwise have been the case. See this example:
lib/example.es6.js
export function fn () {};
lib/invokeExample.es6.js
import { fn } from './example'; // Export something that allows useful stubbing. export var __ = { exampleFn: fn }; // Usage. export function invokeExample () { return __.exampleFn(); };
test/lib/invokeExample.spec.es6.js
import sinon from 'sinon'; import * as invokeExample from '../../lib/invokeExample'; // The example function can only be stubbed because additional code was // written to export it and use the export explicitly inside invokeExample. sinon.stub(invokeExample.__, 'exampleFn').return({});
This boilerplate is unnecessary; it goes away if a sensible import statement is used, as in the following example:
lib/example.es6.js
export function fn () {};
lib/invokeExample.es6.js
import * as example from './example'; // Usage. export function invokeExample () { return example.fn(); };
test/lib/invokeExample.spec.es6.js
import sinon from 'sinon'; import * as example from '../../lib/example'; import * as invokeExample from '../../lib/invokeExample'; // The example function can be stubbed directly, and the // stub will now be used when invokeExample.invokeExample // is called. sinon.stub(example, 'fn').return({});
In Short, Avoid the Use of "import { x } from 'y'"
The bottom line is that "import { x } from 'y'" should be largely or completely absent from ES6 code in order to make the code both clean and testable. Yes, it's the new new thing, but no, it doesn't make life any better.
// Do this. import * as example from 'example'; example.fn(); // Don't do this. // import { fn } from 'example'; // fn();