DEV Community

WellPaidGeek šŸ”„šŸ¤“šŸ”„
WellPaidGeek šŸ”„šŸ¤“šŸ”„

Posted on • Originally published at wellpaidgeek.com

JavaScript: ES6 modules made simple

Before ES6 there was no native system in JavaScript for importing modules.

There were tools like commonjs, but nothing built into the language spec. Every other major language seems to have a way to do this, so the fact that JavaScript lacked this did lend credence to the people who thought of JavaScript as a ā€˜toy languageā€™.

In this article Iā€™m going to be looking at why we need modules in JavaScript, and how we create and use them.

Why do we need modules?

Without modules, by default all code included in our application, either from 3rd party code or our own, would be global in scope by default.

Modern JavaScript applications can use many thousands of imported functions (not just the libraries you use, but the libraries they use and so on). If everything was global, thatā€™s one hell of a cluttered global namespace. Youā€™d forever fear that each time you created a new function there would be a naming clash. The best case is that you get an error as soon as you define something thatā€™s got a name thatā€™s taken. Worse case is it gets silently overwritten, leading to a very very hard to find bug.

The revealing module pattern

In the past this has been solved on an ad hoc basis, usually using the revealing module pattern. An example of this pattern would be this:

const public = (function () {
  var hidden = true;
  function private1 () {}
  function private2 () {}
  return {
    private1,
    private2,
  };
})();

The result of this is that private1, private2 and hidden are private to the scope of the enclosing function. They do not exist in the global scope. All that exists in the global scope is public. ā€˜publicā€™ is a variable referencing an object that has properties called private1 and private2. These are functions which we are exporting from the ā€˜moduleā€™.

Although this solution worked, there were a few problems with it:

  • Having to do the self executing closure is annoying, ugly boilerplate
  • Since itā€™s not an ā€˜officialā€™ built into the language standard, 3rd party code may not do it at all
  • Lack of a standard means different libraries may implement this differently, leading to confusion.

To solve these problems, ES6 gave us modules.

Default Exports

An ES6 module is just a JavaScript file which exports certain expressions which can then be imported elsewhere in your code.

Exports can be default or named. Letā€™s look at default exports first.

const secretNumber = 123;
export default class User;

A default export is done by using the export keyword followed by the default keyword, followed by the expression to be exported, in this case the User class definition.

Default exports are imported as follows:

import User from './user';
const user = new User('wellpaidgeek@gmail.com');

Here the user would be defined and exported in one js file, and imported and used in another js file. Each js file would be its own module.

The path to user when used in the import statement ('./user') should be the relative path to that file from the current file youā€™re importing to.

Note with the default exports, what we choose to name the thing weā€™re importing is completely arbitrary. It doesnā€™t have to match whatever we called it when we exported it. This means the above could be written as the following, and will still work just the same:

import ICanCallThisAnythingAndItIsStillAUserClass from './user';
const user = new ICanCallThisAnythingAndItIsStillAUserClass('wellpaidgeek@gmail.com');

A module does not have to have a default export, but if it does, it can only have one of them. So the following is invalid:

const func1 = () => {};
const func2 = () => {};

export default func1;
export default func2;

What types of thing can we export?

Any expression. So thatā€™s variables, functions, classes, literals. All of the following are valid default exports:

export default 99;
export default 'foo';
export default 10 + 10;
export default () => { console.log('EXPORTED'); };
const x = 10;
export default x;

Named exports

The other type of exports we can have are called named exports. An example is as follows:

// maths.js
export const pi = 3.142;
export const factorial = x => {
    if (x < 2) {
        return 1;
    }
    return x * factorial(x - 1);
};

// main.js
import { pi, factorial } from './maths';

const myNumber = factorial(4) + pi;

ā€˜maths.jsā€™ is exporting two named exports, pi and factorial. ā€˜main.js ā€˜ is using them.

Unlike with default exports where each module can only have one default export, a module can have any number of named exports. The other difference is that named exports must be given a name, and they must be imported using that name. When we import named exports, the names of all the exports we want to import must be included in a comma separated list, wrapped in curly braces.

How do we give an export a name? An exportā€™s name is taken to be the identifier we use for the expression. This could be a function name, variable / constant name or class name. In the case of maths.js, constant names are used.

Other examples of naming:

export class User {} // name: User
export function generatePassword () {} // name: generatePassword
export const apiKey = '123'; // name: apiKey

Mixing default and named exports

What if we want a module to have both a default export and also named exports? This is easy, and would work like this:

// user.js
export default class User {}

export function generatePassword () {}
export const generateUniqueUserId = () => {};

// main.js
import User, { generatePassword, generateUniqueUserid } from './user';

The default import must come first, then a comma, then the list of named exports we want, enclosed in curly brackets.

Aliasing named imports

You may have notice a flaw in named imports. What if we import something and it has a naming clash with another module? Donā€™t worry, the clever people behind ES6 have thought of that. They have given us the ability to alias named exports.

If we had two modules, module1 and module2, and they each had an export named ā€˜calculateā€™, here is how we would alias them to avoid a naming clash in the module importing them:

import { calculate as module1Calculate } from './module1';
import { calculate as module2Calculate } from './module2';

module1Calculate();
module2Calculate();

Using modules

In modern browsers like chrome, you can use modules by specifying type=ā€œmoduleā€ in the script tag when including them in an HTML page. If you had a module called user, and a module called main that imported from user, youā€™d include them like this in your webpage:

<script type=ā€moduleā€ src=ā€user.jsā€></script>
<script type=ā€moduleā€ src=ā€main.jsā€></script>

Although Iā€™m aware this is possible, I never do this, mainly as this is not fully supported in all browsers yet. Instead I use a combination of webpack and babel to compile all modules into a single bundle for deployment. This is beyond the scope of this article (Itā€™s long enough already!). An easy way to try this out would be to use create react app to create a barebones react app. You could then create modules in the src folder and practice importing from them into App.js.

Liked this? Then you'll love my mailing list. I have a regular newsletter on JavaScript, tech and careers. Join over 5,000 people who enjoy reading it. Signup to my list here.

Top comments (0)