An Intro to JavaScript Modules

bajcmartinez

Juan Cruz Martinez

Posted on July 8, 2020

An Intro to JavaScript Modules

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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() { ... };
Enter fullscreen mode Exit fullscreen mode

Default exports:

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

// export individual features as default
export default function () { ... } 
export default class { .. }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Importing a named export

import { var1, var2 } from 'mymodule'

console.log(var1)
console.log(var2)
Enter fullscreen mode Exit fullscreen mode

Renaming an import

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

// Now myvar will be available instead of var1
console.log(myvar)
console.log(var2)
Enter fullscreen mode Exit fullscreen mode

Importing all from a module

import * as anyName from 'mymodule'

console.log(anyName.var1)
console.log(anyName.var2)
console.log(anyName.default)
Enter fullscreen mode Exit fullscreen mode

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.
  });
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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!


If you like the story, please don't forget to subscribe to our free newsletter so we can stay connected: https://livecodestream.dev/subscribe

💖 💪 🙅 🚩
bajcmartinez
Juan Cruz Martinez

Posted on July 8, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related