ES Modules
ES modules (ESM) are the official standard for organizing JavaScript into reusable files. Learn import/export syntax, module loading, and how ESM differs from CommonJS.
Why Modules?
Before modules, all JavaScript on a page shared a single global scope. Libraries would pollute global variables, and there was no standard way to share code between files. Modules solve this:
- Each module has its own scope
- You explicitly choose what to export and import
- Dependencies are explicit โ no hidden globals
- Enables tree-shaking (dead code elimination)
Named Exports
Export multiple values from a module:
// math.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
export function mul(a, b) { return a * b; }
export const PI = 3.14159;
// Or export at the end:
function divide(a, b) { return a / b; }
const E = 2.71828;
export { divide, E };
Import specific names:
// main.js
import { add, sub, PI } from "./math.js";
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
// Rename on import
import { add as sum, PI as pi } from "./math.js";
Default Exports
Each module can have one default export โ the "main" thing the module provides:
// user.js
export default class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// Or a function:
// export default function createUser(name) { ... }
Import without braces (any name you choose):
import User from "./user.js";
import CreateUser from "./user.js"; // same export, different name โ valid
const alice = new User("Alice");
console.log(alice.greet());
Named + Default Together
// config.js
export const VERSION = "1.0.0";
export const DEBUG = false;
export default {
apiUrl: "https://api.example.com",
timeout: 5000,
};
import config, { VERSION, DEBUG } from "./config.js";
console.log(VERSION, config.apiUrl);
Import All as Namespace
import * as math from "./math.js";
console.log(math.add(2, 3));
console.log(math.PI);
Re-exporting
Modules can re-export from other modules โ useful for creating "barrel" files:
// utils/index.js โ barrel file
export { add, sub } from "./math.js";
export { formatDate } from "./date.js";
export { clamp } from "./number.js";
export { default as User } from "./user.js";
// Now consumers can import from one place:
import { add, formatDate, User } from "./utils/index.js";
Dynamic Imports
Load modules lazily, on demand:
// Loads only when needed
async function loadChart() {
const { Chart } = await import("./chart.js");
return new Chart(data);
}
// Conditional loading
async function loadFeature() {
if (user.isPremium) {
const { PremiumFeature } = await import("./premium.js");
return new PremiumFeature();
}
return null;
}
Dynamic imports return Promises and enable code splitting โ a key optimization in modern bundlers.
Module Characteristics
// modules are always in strict mode (no "use strict" needed)
// modules execute once, then are cached
// this === undefined at the top level
// await can be used at the top level in modules (ES2022)
// top-level await (in module):
const data = await fetch("/api/config").then(r => r.json());
export const config = data;
ESM vs CommonJS (CJS)
Node.js originally used CommonJS. Understanding the difference matters when working in Node.js environments:
// CommonJS (Node.js traditional)
const fs = require("fs"); // synchronous, always
module.exports = { myFunction }; // export
const { myFunction } = require("./module"); // import
// ES Modules (modern standard)
import fs from "fs"; // static, analyzed at parse time
export { myFunction }; // export
import { myFunction } from "./module.js"; // import
// Key differences:
// - ESM: static (analyzed before execution), async loading, tree-shakeable
// - CJS: dynamic (can require() inside conditions), synchronous
// - ESM files: .mjs extension, or "type": "module" in package.json
๐กIn Next.js and React projects
When using a bundler (Webpack, Vite, etc.), you always use ESM syntax (import/export). The bundler handles compatibility with CJS libraries automatically. Don't use require() in modern frontend code.
Circular Dependencies
Modules can reference each other, but circular dependencies can cause issues:
// a.js
import { b } from "./b.js";
export const a = "A: " + b; // b might be undefined!
// b.js
import { a } from "./a.js";
export const b = "B: " + a; // a might be undefined!
ESM handles circular dependencies with live bindings, but values may not be initialized yet when first accessed. Avoid circular dependencies when possible.
Key Takeaways
exportexposes values from a module;importbrings them in- Named exports:
export { name }/import { name }โ use{} - Default export:
export default value/import anythingโ no{} - Dynamic
import()loads modules lazily (returns a Promise) - Barrel files (
index.js) re-export from multiple modules for clean import paths - ESM is static and tree-shakeable; CJS is dynamic and synchronous
Ready to test your knowledge?
Take a quiz on what you just learned.