Overview

Modules are small units of independent, reusable code that is desired to be used as the building blocks in creating a non-trivial Javascript application. Modules let the developer define private and public members separately, making it one of the more desired design patterns in JavaScript paradigm.

Export and import directives are very versatile.

Export before declarations

We can label any declaration as exported by placing export before it, be it a variable, function or a class.

For instance, here all exports are valid:

// export an array
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// export a constant
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// export a class
export class User {
  constructor(name) {
    this.name = name;
  }
}

Please note that export before a class or a function does not make it a function expression. It’s still a function declaration, albeit exported.

Most JavaScript style guides recommend semicolons after statements, but not after function and class declarations.

That’s why there should be no semicolons at the end of export class and export function.

export function sayHi(user) {
  alert(`Hello, ${user}!`);
}  // no ; at the end

Export apart from declarations

Also, we can put export separately.

Here we first declare, and then export:

// 📁 say.js
function sayHi(user) {
  alert(`Hello, ${user}!`);
}

function sayBye(user) {
  alert(`Bye, ${user}!`);
}

export {sayHi, sayBye}; // a list of exported variables

Import

Usually, we put a list of what to import into import {...}, like this:

// 📁 main.js
import {sayHi, sayBye} from './say.js';

sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!

But if the list is long, we can import everything as an object using import * as <obj>, for instance:

// 📁 main.js
import * as say from './say.js';

say.sayHi('John');
say.sayBye('John');

At first sight, “import everything” seems such a cool thing, short to write, why should we ever explicitly list what we need to import?

Well, there are few reasons.

  1. Modern build tools (webpack and others) bundle modules together and optimize them to speedup loading and remove unused stuff.Let’s say, we added a 3rd-party library lib.js to our project with many functions:
/ 📁 lib.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
  1. …Then the optimizer will automatically detect it and totally remove the other functions from the bundled code, thus making the build smaller. That is called “tree-shaking”.
  2. Explicitly listing what to import gives shorter names: sayHi() instead of lib.sayHi().
  3. Explicit imports give better overview of the code structure: what is used and where. It makes code support and refactoring easier.

Import “as”

We can also use as to import under different names.

For instance, let’s import sayHi into the local variable hi for brevity, and same for sayBye:


// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';

hi('John'); // Hello, John!
bye('John'); // Bye, John!

Export “as”

The similar syntax exists for export.

Let’s export functions as hi and bye:

// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
// 📁 main.js
import * as say from './say.js';

say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!

export default

So far, we’ve seen how to import/export multiple things, optionally “as” other names.

In practice, modules contain either:

  • A library, pack of functions, like lib.js.
  • Or an entity, like class User is described in user.js, the whole module has only this class.

Mostly, the second approach is preferred, so that every “thing” resides in its own module.

Naturally, that requires a lot of files, as everything wants its own module, but that’s not a problem at all. Actually, code navigation becomes easier, if files are well-named and structured into folders.

Modules provide special export default syntax to make “one thing per module” way look better.

It requires following export and import statements:

  1. Put export default before the “main export” of the module.
  2. Call import without curly braces.

For instance, here user.js exports class User:

// 📁 user.js
export default class User { // just add "default"
  constructor(name) {
    this.name = name;
  }
}
// 📁 main.js
import User from './user.js'; // not {User}, just User

new User('John');

Imports without curly braces look nicer. A common mistake when starting to use modules is to forget curly braces at all. So, remember, import needs curly braces for named imports and doesn’t need them for the default one.

Naturally, there may be only one “default” export per file.

We may have both default and named exports in a single module, but in practice people usually don’t mix them. A module has either named exports or the default one.

Another thing to note is that named exports must (naturally) have a name, while export default may be anonymous.

For instance, these are all perfectly valid default exports:

export default class { // no class name
  constructor() { ... }
}

export default function(user) { // no function name
  alert(`Hello, ${user}!`);
}

// export a single value, without making a variable
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];