Use Modules to Build a Modular JavaScript App

One of the big features of ES6 is JavaScript supporting built-in modules. Modules allow us to share code between files by using the export and import syntax. This is a big improvement over using script tags and global variables to share code across files.

Using script tags was error prone since the load order matters. Having scripts in the wrong order could cause our program to execute code that hasn’t been declared yet. It also forced us to write spaghetti code with no real structure or logical composition. This problem doesn’t exist with modules because everything is exported and imported directly between files. Also, we can know the definition of the imported code easily since it’s explicit in which modules is being imported and referenced.

Exports and Imports

To make code in a JavaScript file importable, we have to export them explicitly with the export statement. To do this, we just put export in front of the variable or constant you want to expose to other files. For example, we can write:

export let num = 1;

This exports the num variable so that other modules can import it and use it.

We can export anything declared with var, let, const, and also functions and classes. The items exported must be declared at the top level. export cannot be used anywhere else, like inside functions and classes.

We can export multiple members at once. All we have to do is wrap all the members in curly brackets separated by commas. For example, we can write:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

This will let us import num1, num2, and num3 in other JavaScript files.

Now that we have exported the members, we can import them in other JavaScript files. We can use the import statement to import one or more members into a module and work with them. For example, if we have the following in moduleA.js:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

Then in moduleB.js we can write the following code to import the items from moduleA.js:

import {num1, num2, num3} from './moduleA'

The path after the from keyword starts with a period. The period means that we’re in the current folder.

This assumes that moduleA.js and moduleB.js are in the same folder. If we have them in different folder, then we have the specify the path of moduleA.js relative to moduleB.js if we want to import the exported members of moduleA.js into moduleB.js. For example, if moduleA.js is one level above moduleB.js, then in moduleB.js we write:

import {num1, num2, num3} from '../moduleAFolder/moduleA'

The 2 periods before the path means that we go up one folder level and then get the moduleAFolder and then get moduleA.js.

We can also use JavaScript modules in script tags. To do this, we must set the type attribute of the script tag to module to use them. For example, if we want to use moduleA.js in our HTML file, we can write:

<script type='module' src='moduleA.js'></script>

We can use import and export in JavaScript modules. They won’t work with regular scripts.

Scripts run in strict mode automatically, so we can’t accidentally declare global variables and do other things that can be done without strict mode being enabled. They also load asynchronously automatically so that we won’t have to worry about long scripts holding up a page from loading. Also, import and export only happens between 2 scripts, so no global variables are set. Therefore, they can’t be viewed in the console directly.

Default Exports

There’s also a default export option for exporting a module’s members. We previously exported the variable in a way where we import them by the name. There’s also the default export which exports a single member from a module without needing to reference it explicitly by name when you’re importing it. For example, if we have a single member in a module that we want to export, we can write the following in moduleA.js:

const num = 1;
export default num;

There are no curly braces when you use export default .

Then in the file where you want to import the member. In moduleB.js we write:

import num from './moduleA'

Once again, we omit the curly braces. This is because only one default export is allowed per module. Alternatively, we can write the following in moduleB.js :

import {default as num} from './moduleA'

Renaming Imports and Exports

If we have many modules and their exported members have the same name, then there will be conflicts if we try to import multiple modules. This is will be a problem that we need to fix. Fortunately, JavaScript has the as keyword to let us rename exports and imports so we can avoid name conflicts in our code. To use the as keyword to rename exports, we write the following in moduleA.js:

export {
  num1 as numOne,
  num2 as numTwo
}

Then in moduleB.js, we can import them by writing:

import { numOne, numTwo } from './`moduleA'`

Alternatively, we can do the renaming when we import instead. To do this, in moduleA.js, we put:

export {
  num1,
  num2
}

Then in moduleB.js, we put:

import { num1 as numOne, num2 as numTwo } from './`moduleA'`

If we try to import members with modules where the members have the same name, like:

`import {` num1, num2 `} from './moduleA';
import {` num1, num2 `} from './moduleB';
import {` num1, num2 `} from './moduleC';`

We will see that we get SyntaxError. So we have to rename them so that the module will run:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

A cleaner way to import from multiple modules that have members with the same names is to import all of the module’s exported members as one object. We can do that with an asterisk. For example, instead of:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

We can instead write:

import * as moduleA from './moduleA';
import * as moduleB from './moduleB';
import * as moduleC from './moduleC';

Then in code below the imports, we can write:

moduleA.num1;
moduleA.num2;
moduleB.num1;
moduleB.num2;
moduleC.num1;
moduleC.num2;

We can also export and import classes. So if we have a file that contains one or more classes, like the Person.js file with the class below:

class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
  get firstName() {
    return this._firstName
  }
  get lastName() {
    return this._lastName
  }
  sayHi() {
    return `Hi, ${this.firstName} ${this.lastName}`
  }
  set firstName(firstName) {
    this._firstName = firstName;
  }
  set lastName(lastName) {
    this._lastName = lastName;
  }
}

Then we write the following to export a class:

export { Person };

This exports the Person class, and then to import it, we write:

import { Person } from './person';

Dynamic Module Loading

JavaScript modules can be loaded dynamically. This lets us only load modules when they’re needed rather than loading all of them when the app runs. To do this, we use the import() function, which returns a promise. When the module in the argument is loaded, then the promise is fulfilled. The promise resolves to a module object, which can then be used by the app’s code. If we have the Person class in Person.js, then we can dynamically import it with the following code:

import('./Person')
.then((module)=>{
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
})

Or using the async and await syntax, we can put that in a function:

const importPerson = async ()=>{
  const module = await import('./Person');
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
}

importPerson();

As you can see, JavaScript modules are very useful for organizing code. It allows us to export things that we want to expose to other modules, eliminating the need for global variables. In addition, exports and imports can be renamed to avoid conflict when importing multiple modules. Also, all the exported members can be imported all at once so that we get the whole module as an object instead of importing individual members. Finally, we can use export default if we only want to export one thing from our module.