Joshua's Cheatsheets - JavaScript Modules - Cheat Sheet and Comparison
Light
help

Resources

What & Link Type
Flavio Copes guides to ES Modules Code examples
Tyler McGinnis's guide to Modules (very comprehensive) Comprehensive blog post
FreeCodeCamp (Preethi Kasireddy) post on Modules, lots of code samples FreeCodeCamp Post
CanIUse for ES6 (ECMAScript 2015), which includes modules Browser Coverage
ES6 vs CommonJS Examples (2ality) Code Examples
Impatient JS: Chapter on Modules (24) (from 2ality)
- Another Link
Book Chapter
Dixin Yan: Understanding (all) JavaScript Module Formats and Tools Guide

What is a module?

Simply put, a module is a grouping of code into a distinct unit (the module) that is separated from other code both in physicality (separate file and/or code wrapping) and in functionality (variable scoping, namespaces, etc.)

Modules are isolated, yet reusable,

Native vs Non-Native

Modules have historically not been supported natively in JS, so many environments (nodejs) used a non-native implementation/flavor of modules, such as CommonJS.

The terminology has gotten a little convoluted over time, but currently there is really only one native JS module format, usually referred to as ES6 Modules or just ES Modules.

  • These are very common, in both JS and TS

There are many non-native module solutions:

  • CommonJS
  • AMD (Asynchronous Module Definition)
  • UMD (Universal Module Definition)

These non-native formats require loaders, such as requirejs, in most environments, such as the browser.

Hand-coded / Generic modules

Because the term "module" is generic and just refers to the idea of isolated building blocks, there are many ways to build modules that don't rely on a standardized definition/spec like CommonJS. In fact, a basic IIFE (Immediately Invoked Function Expression) could be considered a module format, since it provides closure/scoped vars:

// Module 'customDebug'
(function(){
	// These variables are local scoped to IIFE/module
	var defaultDebugLevel = 1;
	var currentDebugLevel = 1;
	var customDebug = {
		setDebugLevel: function(level){
			currentDebugLevel = level;
		},
		logMsg: function(msg,dbgLevel){
			if (dbgLevel <= currentDebugLevel){
				console.log(msg);
			}
		}
	}
	// This will expose module to global namespace
	App = typeof(App)!=='undefined' ? App : {};
	App.customDebug = customDebug;
})()

Syntax differences between module options:

Module Type Import Syntax Export Syntax Notes
CommonJS const fs = require('fse');

Using ES6 Object Destructuring:
const {foo, bar} = require('myfile.js');
Per file, define module.exports (can be function, object, etc.)

If you want to emulate ES6 export default, just assign a single thing to module.exports, e.g. module.exports = function(){};

Short syntax (module.exports.myVar = 'test') can often be used
- Used by NodeJS
- Default is one def per file
AMD require(['dependencyAlpha','dependencyBeta'],function(depA,depB){ // Do something }); define('moduleName',['dependencyAlpha'],function(depA){ // Return what the module should be equal to }); - Unlike CJS, can have multiple defs per file
UMD UMD Example from FreeCodeCamp
From FreeCodeCamp
Depends on environment, see code example for import to see how it exposes the factory - Basically a combination of AMD+CommonJS.
- Flexibility allows it to work in multiple environments
- Trade-off is less legible generated code
ES Modules import ModuleA from 'my-modules';
import {ModuleA,ModuleB} from 'my-modules';
import * as myModules from 'my-modules';

Interop with CommonJS single export in TypeScript (see docs):
import MyModules = require('my-modules');

Dynamic importing* (limited support) via:
import('./lib/my-module').then((module)=>{});
or:
await module = await import('./lib/my-module');

Importing via <script type="module"> tag in the browser is on the way.
Just prefix basically anything with the export or export default keyword.

export const getUsernameById = (userId) => {//...};
- You can have multiple named exports per file, one default per file
- Named exports are preferred over export default, for tree-shaking

ES Modules - further notes

Dynamic imports

As noted in the table above, dynamic imports in ES Modules is a newer feature, that is not fully supported everywhere.

  • Node:

    • Supported V9.7+ (reminder, you can check from CLI with node -v)
  • Browser:

    • A TC39 proposal, which should land fully in ES2020
    • CanIUse link - As of 10/24/2019, around 84% support

Here is a nice write-up of the feature and some examples on how to use it: v8.dev

Why default export is bad

If you start looking up information about ES Module exports, you will likely start seeing posts and headlines talking about why export default is bad and should be avoided.

Here is a brief summary of the faults of export default

  • Makes it harder, or impossible, for bundling / minifying programs to "tree-shake", e.g. remove dead un-used code.

    • If you are concerned about performance and keeping request size down, this is important.
  • (Arguable) - Decreases readability and increases ambiguity

    • To me, using default exports all over the place can lead to some readiblity issues, because it tends to encourage aliasing and renaming. Example:

      • In one file, a dev might choose to use import * as couch from './furniture/sku-24.js'
      • In another file, another dev might choose to import the same default as import * sofa from './furniture/sku-24.js'
      • If I'm just eye-balling these files, and not looking at the top of every file, I might not notice at first that these are pulling from the same file. And searching for specific imports might give me trouble too.
  • (Arguable) - Although it makes refactoring harder, it also makes it more explicit

Benefits of export default

  • Easier to refactor

    • Because you can easily alias without knowing the actual name of the export within the file, if that export is changed, you don't need to update the files that consume it
    • However, this same benefit could be achieved by using a class as a named export instead of default, and changing the names of methods in the class instead
  • Can make writing code a tiny bit faster, since there is slightly less boilerplate

Due to the mix of drawbacks and benefits, some people prefer to use a mix. For example, if your file is mostly centered around a class, let's say Person, you might export the class as the default, but then export things related to that class as named exports. For example:

export default class Book {
	constructor({name, author}) {
		this.name = name;
		this.author = author;
	}
	//.. tons of members, methods, etc.
}

// Named export
export const oz = new Book({
	name: 'The Wonderful Wizard of Oz',
	author: 'L. Frank Baum'
});

If you are looking for a thread full of arguments on the topic, this Airbnb JS style guide issue thread is a pretty lively read.

Warnings, Common Issues, FAQ

Be careful about side effects and code outside defined exports

When you use the "files as modules" pattern to structure your app (where there is a maximum of one module per file), it is easy to forget that files are not modules themselves, and if you "do something" outside of code that is wrapped in an export, any time you import from that file, that code is potentially triggering other things to happen, importing, etc.

This is also an easy way to trigger the ___.default is not a constructor error.

How to Import JSON?

Usually, this is pretty easy with standard bundling tools:

For CommonJS, this should work across the board:

const config = require('./config.json');

But with ES Imports, this will only work with certain (common) loaders:

import * as config from './config.json';

And for webpack (e.g. for CRA), this should work:

import config from './config.json';

For TypeScript, you will need to set resolveJsonModule to true, either via CLI flag, or via tsconfig.

Why Use ES6 Modules / ES Modules

There are a bunch of benefits to ES Modules:

  • Static in nature

    • This has a lot of side-benefits, related to type-checking, transpiling, supporting async loading, and tree-shaking
  • Eventually (as it gains adoption), will make writing native cross-environment (aka isomorphic, NodeJS + Browser) code easier
  • Standardized syntax (arguable)
Markdown Source Last Updated:
Mon Jul 27 2020 22:01:53 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Wed Aug 21 2019 00:46:11 GMT+0000 (Coordinated Universal Time)
© 2020 Joshua Tzucker, Built with Gatsby