ES6 Modules Explained

March 23, 2019

While building a web app, it is always important to divide our project into modularized chunks in order to reuse and share code. However, if you have used an older version of JavaScript before, you might remember that exporting and importing used to be quite cumbersome and confusing with multiple “standards” and bundlers fighting against each other.

The good news is, ES6 modules syntax is here to standardize and simplify the process! Also, all mainstream browsers nowadays already have pretty nice ES6 modules support, so in most cases, we don’t even need to transpile or bundle our code.

In this article, I will explain how to use the powerful new modules syntax in modern JavaScript introduced since ES6.

Before we get started, let’s assume that we have a huge single tv.js file containing all of the code that we have written. We will be focusing on the following portion of the code. (The code doesn’t make much sense, it’s only for demo purposes. :P)

// tv.js

// Some options
const CHANNELS = ['none', 'CNN', 'BBC'];
let currentChannel = CHANNELS[0];

// A handy function to update the currentChannel
function lookupChannel(index) {
  return CHANNELS[index];
}

// A class with a variable and a pair of setter and getter
class Settings {
  _volume = 50

  set volume(newVolume) {
    this._volume = newVolume
  }

  get volume() {
    return this._volume
  }
}

// An initialized "Context" object that is supposed to be visible globally
const globalSettings = new Settings();

Our mission is to refactor the project and extract all those elements above to their own, corresponding files.

Default Export

As the name suggests, default export is the simplest, de facto way to expose a single piece of code to the outside world.

Let’s say we want to put the array constant CHANNELS inside its own file channels.js. We can achieve that with a default export statement like this:

// `channels.js`
const CHANNELS = ['none', 'CNN', 'BBC'];

export default CHANNELS;

Correspondingly, we can simply import the constant inside tv.js like below, then use it normally.

// `tv.js`
import CHANNELS from './channels.js';

let currentChannel = CHANNELS[0];

// A handy function to update the currentChannel
function lookupChannel(index) {
  return CHANNELS[index];
}

// ...

One important thing to notice is that one module can only have a single default export. When we are importing, it actually doesn’t matter if we have given the imported element the same name as the one it has in its own module - the name of the exported element is identified by the module file itself. Thus, something like this is totally legal:

// `tv.js`
import OPTIONS from './channels.js';

let currentChannel = OPTIONS[0]; // OPTIONS instead of CHANNELS

// ...

Because the name doesn’t matter with default exports, anonymous functions, constants or classes are also allowed.

For example, we can export the Settings class anonymously like this:

// `Settings.js`
export default class {
  _volume = 50;

  set volume(newVolume) {
    this._volume = newVolume;
  }

  get volume() {
    return this._volume;
  }
};
// `tv.js`
import Settings from './Settings.js';

// ...

const globalSettings = new Settings();

We can also export the globalSettings as a anonymous constant like below.

// `globalSettings.js`
import Settings from './Settings.js';

export default new Settings();
// `tv.js`
import globalSettings from './globalSettings.js';

// Let's do some actions
globalSettings.volume = 100;

Named Export/Import

If we would like to export more than one element from a single module, we will then need to use named exports. One way is to simply remove the default keyword, then wrap the elements inside an object; the other way is to add an “export” keyword before each element that we want to export.

// `channelUtils.js`

// === Method 1 ===
// Wrap everything inside an object:
const CHANNELS = ['none', 'CNN', 'BBC'];

function lookupChannel(index) {
  return CHANNELS[index];
}

export { CHANNELS, lookupChannel };

// === Method 2 ===
// Export everything on the go:
export const CHANNELS = ['none', 'CNN', 'BBC'];

export function lookupChannel(index) {
  return CHANNELS[index];
}

// Nothing else required!

In order to import a named export, we will need to destructure it like this:

// `tv.js`
import { CHANNELS, lookupChannel } from 'channelUtils.js';

let currentChannel = CHANNELS[0];

We can also rename the imported element with the as keyword like this:

// `tv.js`
import { CHANNELS as OPTIONS } from 'channelUtils.js';

let currentChannel = OPTIONS[0];

Mixing Named and Default Exports

We can also mix the two ways of import/exports together!

// `channelUtils.js`

export const CHANNELS = ['none', 'CNN', 'BBC'];

export function lookupChannel(index) {
  return CHANNELS[index];
}

const moduleName = `channelUtils.js`;

export default moduleName;
// `tv.js`
import defaultImportLOL, { CHANNELS, lookupChannel } from 'channelUtils.js';

let currentChannel = CHANNELS[0];

console.log(defaultImportLOL); // outputs `channelUtils.js`

Use * for Everything!

Another cool but less often used feature for imports is to use * to import everything from a module as a single object.

Let’s reuse the mixed exports example above:

// `channelUtils.js`

export const CHANNELS = ['none', 'CNN', 'BBC'];

export function lookupChannel(index) {
  return CHANNELS[index];
}

const moduleName = `channelUtils.js`;

export default moduleName;

With *, we can do this:

// `tv.js`
import * as awesome from 'channelUtils.js';

let currentChannel = awesome.CHANNELS[0];

console.log(awesome.default); // outputs `channelUtils.js`

Note that if we use *, we will have to alias the whole module to a name. The default key of the module object always refers to the element that is exported with the default keyword.

Conclusion

This is how we can use the ES6 modules syntax. Hopefully this article has helped you clear up some confusions. Let’s write some more modularized code!