ECMAScript (aka ES2015 or ES) modules are a way to organize cohesive blocks of code in JavaScript.

The ES module system has 2 parts.

  • import module - use import { func } from '. /myModule'
  • export module - uses export const func = () => {}

An import module is a module that imports dependencies using the import syntax.

1
2
3
import { concat } from './concatModule';

concat('a', 'b'); // => 'ab'

The imported module uses the export syntax to export components from itself.

1
export const concat = (paramA, paramB) => paramA + paramB;

import { concat } from '. /concatModule' The way to use ES modules is static: meaning that the dependencies between modules are known at compile time.

While static import is valid in most cases, sometimes we want to save our clients bandwidth and load modules conditionally.

To achieve this, we can do a new dynamic import of the module in a different way using the import(pathToModule) syntax: as a function. Dynamic importing is a JavaScript language feature that started with ES2020.

1. Dynamic module import

When the import keyword is used as a function instead of the static import syntax:

1
const module = await import(pathToModule);

It returns a promise and starts an asynchronous task to load the module. If the module is successfully loaded, then promise resolves to the module’s content, otherwise, promise is rejected.

Note that pathToModule can be any expression whose value is a string indicating the path to the imported module. Valid values are plain string literals (e.g. . /myModule) or variables with strings.

For example, we load a module in an asynchronous function.

1
2
3
4
5
6
async function loadMyModule() {
  const myModule = await import('./myModule');
  // ... use myModule
}

loadMyModule();

Interestingly, in contrast to static import, dynamic import accepts expressions that are evaluated by module paths.

1
2
3
4
5
6
async function loadMyModule(pathToModule) {
  const myModule = await import(pathToModule);
  // ... use myModule
}

loadMyModule('./myModule');

Now, after understanding how to load a module, let’s see how to extract components from the imported module.

2. Importing components

2.1 Importing named components

Consider the following module.

1
2
// namedConcat.js
export const concat = (paramA, paramB) => paramA + paramB;

A concat function is exported here.

If you want to dynamically import namedConcat.js and access the named exported concat, then simply deconstruct it as follows.

1
2
3
4
5
6
async function loadMyModule() {
  const { concat } = await import('./namedConcat');
  concat('b', 'c'); // => 'bc'
}

loadMyModule();

2.2 Default Export

If the module is exported by default, we can access it using the default attribute.

As in the example above, we export the concat function inside defaultConcat.js by default:

1
2
// defaultConcat.js
export default (paramA, paramB) => paramA + paramB;

In the dynamic import module, you can use the default attribute to access it.

1
2
3
4
5
6
async function loadMyModule() {
  const { default: defaultImport } = await import('./defaultConcat');
  defaultImport('b', 'c'); // => 'bc'
}

loadMyModule();

Note that default is a keyword in JavaScript, so it cannot be used as a variable name.

2.3 Importing mixed forms

If the module has both default and named exports inside it, the same decomposition is used to access it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async function loadMyModule() {
  const { 
    default: defaultImport,
    namedExport1,
    namedExport2
  } = await import('./mixedExportModule');
  // ...
}

loadMyModule();

3. When to use dynamic import

It is recommended to use Dynamic Import for larger modules or for modules that are imported on a conditional basis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async function execBigModule(condition) {
  if (condition) {
    const { funcA } = await import('./bigModuleA');
    funcA();
  } else {
    const { funcB } = await import('./bigModuleB');
    funcB();
  }
}

execBigModule(true);

For small modules (such as namedConcat.js or defaultConcat.js in the previous example), there are only a few dozen lines of code, so using dynamic import is a bit overkill.

Summary

When import(pathToModule) is called as a function whose argument represents a module specifier (aka path), then the module is dynamically loaded.

In this case, module = await import(pathToModule) returns a promise that resolves to an object containing the components of the imported module.

Dynamic import is supported by Node.js (version 13.2 and above) and most modern browsers.