An Intro to JavaScript Modules

Learn how to use the export and import statements

Feature Image

There seems to be some confusion when it comes to JavaScript modules and how they exactly work, and why are there different forms in which we can use them. Today I’ll explain the different ways in which you can export and import modules.

Some background on JavaScript modules

JavaScript programs started as simple scripts or apps with rather small codebases, but as it has been evolving and so its uses have been increasing the size of the codebases have increased drastically. To support this increase the language needed to support a mechanism under which was possible to separate or split the code into smaller, reusable units. Node.JS had that ability for a while before it was incorporated in JavaScript with a feature called modules. And thus eventually they made it to the language itself and the browsers.

By definition, a module is just a file which can be imported from other modules (or files) through the help of directives like export and import:

  • export: keyword labels variables and functions that should be accessible from outside the current module.
  • import: allows the import of functionality from other modules.

We’ll come back to more of that later.


Introducing an example

To demonstrate the use of modules we will create a simple user module that will expose a User class. Let’s review the basic structure for the project:

index.html
scripts/
    index.js
    modules/
        user.js

Our app will be very simple and it will just show the name of a user on the screen, but the interesting part is that the name will come from an object instance of the User class. Let’s see it in action with a live demo:

Let’s look in detail what’s going on there by parts

Exporting module User

The first thing we need to do to access the User class is to export it from the module. For that, we make use of the export statement.

The export statement is used when creating JavaScript modules to export live bindings to functions, objects, or primitive values from the module so they can be used by other programs with the import statement.

Let’s see that in our code:

// file: scripts/modules/user.js
export class User {
  constructor(name) {
    this.name = name;
  }
}

Now that the module was exported we can use it in other modules by importing it.

Importing module User

The static import statement is used to import read-only live bindings which are exported by another module. Imported modules are in strict mode whether you declare them as such or not. The import statement cannot be used in embedded scripts unless such a script has a type="module”. Bindings imported are called live bindings because they are updated by the module that exported the binding.

Let’s see it in our example

//file: scripts/index.js
import { User } from './modules/user.js'

const user = new User('Juan')

document.getElementById('user-name').innerText = user.name;

The import statement allows us to import specific bindings from a module. There are several different ways to specify what we are importing, and we will discuss them later in the post. For now, in our example, we are just importing User from the specified module (or file).

After importing we can use that object as it is part of the same file.


Default exports versus named exports

So far we exported a class by its name, but there are 2 different way to export out of modules

  • Named Exports (Zero or more exports per module)
  • Default Exports (Only one per module)

Here are some examples of named exports:

// export features declared earlier
export { myFunction, myVariable }; 

// export individual features (can export var, let, const, function, class)
export let myVariable = Math.sqrt(2);
export function myFunction() { ... };

Default exports:

// export feature declared earlier as default
export { myFunction as default };

// export individual features as default
export default function () { ... } 
export default class { .. }

Named exports are useful to export several values. During the import, it is mandatory to use the same name as the corresponding object. But a default export can be imported with any name for example:

// file: myk.js
const k = 12
export default k
// file: main.js
import m from './myk'
console.log(m)

When using named exports, it is also possible to assign a custom name to the exported value like in the following example:

const name = 'value'
export {
  name as newName
}

The value exported can now be imported as newName rather than name.


Importing

We already saw a few examples of how we can import either named or default exports from modules. But here are more options when it comes to importing.

Importing a default export

import something from 'mymodule'

console.log(something)

Importing a named export

import { var1, var2 } from 'mymodule'

console.log(var1)
console.log(var2)

Renaming an import

import { var1 as myvar, var2 } from 'mymodule'

// Now myvar will be available instead of var1
console.log(myvar)
console.log(var2)

Importing all from a module

import * as anyName from 'mymodule'

console.log(anyName.var1)
console.log(anyName.var2)
console.log(anyName.default)

So far all the ways we described here are static imports, meaning that you place them on top of your file and the contents of the module are always imported. But it doesn’t have to be the case, you can also have dynamic imports.


Dynamic imports

This allows you to dynamically load modules only when they are needed, rather than having to load everything upfront. This has some obvious performance advantages; let’s read on and see how it works.

This new functionality allows you to call import() as a function, passing it the path to the module as a parameter. It returns a Promise, which fulfills with a module object giving you access to that object’s exports, e.g.

import('./modules/myModule.js')
  .then((module) => {
    // Do something with the module.
  });

Combining default and named exports

You read it right! it is possible to combine default and named and as you may expect, you can import both of them. Let’s see an example:

//file: mymodule.js
export const named = 'named export'

export function test() {
  console.log('exported function')
}

export default 'default export';

And we can import them using either of the following scenarios:

//another file:
import anyName from './mymodule' // where anyName is the default export

// or both named exports
import { named, test } from './mymodule';

// or just one
import { named } from './mymodule';

// or all of them together
import anyName, { named, test } from './mymodule';

Conclusion

JavaScript modules are a powerful feature that allows us to better organize our code, but it also allows us to share modules across projects. I hope you enjoyed and learned something new today.

Thanks for reading!

Join the Free Newsletter

A free, weekly e-mail with the best new articles, courses, and special bonuses.

We won't send you spam. Unsubscribe at any time.