ES6+ Modules
Checking access...
Modules let you split your code into separate files, each with its own scope. Before ES6 modules, JavaScript had no built-in module system — developers relied on scripts, IIFEs, or third-party solutions like CommonJS (Node.js) and AMD.
Module Basics
A JavaScript file using the export or import keyword becomes a module. Module scopes are isolated — variables and functions are private by default.
Exporting
Named exports — export multiple values from a module:
export const PI = 3.14159;
export function add(a, b) { return a + b;}
export function multiply(a, b) { return a * b;}Default export — export a single main value:
export default function log(message) { console.log(`[LOG] ${message}`);}// Can also export default an object or class:// export default class Logger { ... }Combined — both default and named in one module:
export default function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); };}
export const VERSION = "1.0.0";export function formatDate(date) { /* ... */ }Importing
// Import named exportsimport { add, multiply, PI } from "./math.js";console.log(add(2, 3)); // 5
// Import with aliasimport { add as sum, multiply as mult } from "./math.js";
// Import default exportimport log from "./logger.js";log("Hello"); // [LOG] Hello
// Import default + namedimport debounce, { VERSION, formatDate } from "./utils.js";
// Import everything as a namespaceimport * as math from "./math.js";console.log(math.add(2, 3)); // 5console.log(math.PI); // 3.14159Named vs Default Exports
| Feature | Named Export | Default Export |
|---|---|---|
| Syntax | export const X = ... | export default X |
| Import syntax | import { X } from ... | import X from ... |
| Alias | import { X as Y } | import { default as X } |
| Per module | Unlimited | One per module |
| Auto-complete | ✅ IDE friendly | ❌ Can be ambiguous |
| Re-export | export { X } from ... | export { default } from ... |
Tip
Prefer named exports for libraries and utilities (clearer imports, better IDE support). Use default exports for the main “purpose” of a file (a single class, component, or function).
Module Scope (Strict Mode)
Modules are automatically in strict mode — no "use strict" declaration needed:
x = 10; // ❌ ReferenceError in strict mode (no var/let/const)Top-level this in modules is undefined, not the global object:
console.log(this); // undefined (not Window/global)Re-exporting (Barrel Pattern)
Re-export to create cleaner public APIs:
// index.js — barrel fileexport { default as Button } from "./Button.js";export { default as Input } from "./Input.js";export { default as Modal } from "./Modal.js";export { useTheme } from "./hooks/useTheme.js";Consumers import from the barrel:
// Instead of:import Button from "./components/Button/Button.js";import Input from "./components/Input/Input.js";
// You can do:import { Button, Input } from "./components/index.js";Selective Re-export
// Re-export only specific named exportsexport { login, logout, register } from "./auth/index.js";Dynamic Imports
Dynamic imports load modules on demand at runtime. They return a Promise:
// Static import (eager — always loads)import { format } from "date-fns";
// Dynamic import (lazy — loads when called)async function loadChart() { const chartModule = await import("./charts/Chart.js"); const chart = new chartModule.Chart(); chart.render();}Use Cases for Dynamic Imports
1. Code splitting — load large libraries only when needed:
async function showEditor() { const { default: Editor } = await import("./Editor.js"); const editor = new Editor("content");}2. Conditional loading — platform or feature-specific code:
if (navigator.userAgent.includes("Chrome")) { const { useChromeFeature } = await import("./chrome-features.js"); useChromeFeature();}3. Lazy routes — in frameworks like React, load page components on demand:
// Framework pattern — not actual Reactconst routes = { "/dashboard": () => import("./pages/Dashboard.js"), "/settings": () => import("./pages/Settings.js"),};Module vs Script Behaviour
| Feature | Module (type="module") | Regular Script |
|---|---|---|
| Default mode | Strict | Non-strict |
Top-level this | undefined | window |
| Scoping | File-scoped | Global |
import/export | ✅ | ❌ |
| Deferred by default | ✅ (like defer) | ❌ (blocking) |
Top-level await | ✅ | ❌ |
In HTML, use <script type="module">:
<script type="module"> import { greet } from "./utils.js"; greet();</script>
<!-- Regular script — cannot use import/export --><script> // import { greet } from "./utils.js"; // ❌ SyntaxError</script>CommonJS vs ES Modules
Node.js originally used CommonJS (require/module.exports). Modern Node.js supports both:
// CommonJS (Node.js default before v14)const fs = require("fs");module.exports = { myFunction };
// ES Modules (package.json needs "type": "module")import fs from "fs";export const myFunction = () => {};Mixing CommonJS and ES Modules
// Import CommonJS from ES Moduleimport fs from "fs"; // works — default import gets the whole module
// Import ES Module from CommonJS (use dynamic import)async function load() { const esModule = await import("./es-module.mjs");}| Aspect | CommonJS | ES Modules |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
| Scope | Runtime | Static (analyzable) |
| Tree-shakeable | ❌ | ✅ |
Top-level await | ❌ | ✅ |
| File extension | .js, .cjs | .js ("type": "module"), .mjs |
Module Bundlers
Bundlers like Webpack, Vite, Rollup, and esbuild take your module files and combine them into optimised bundles for production:
Development: Production:math.js ─┐logger.js ─┤ bundle.js (minified)utils.js ─┼──────→ (single file)app.js ───┤styles.css ┘Bundlers enable:
- Tree shaking — remove unused exports
- Code splitting — split into multiple bundles loaded on demand
- Minification — compress code for faster downloads
- Asset handling — import CSS, images, fonts as modules
Vite is the modern standard for frontend projects:
npm create vite@latest my-app -- --template vanillaModule Organisation Best Practices
1. Single Responsibility
Each module should have one clear purpose:
// ✅ Goodauth.js — login, logout, registervalidation.js — input validation functionsapi.js — HTTP request helpers
// ❌ Avoidutils.js — random collection of helpershelpers.js — same problem2. Clear Public API
Export only what consumers need. Keep internals private:
// Private — not exportedconst sessions = new Map();function generateToken() { /* ... */ }
// Public — exported APIexport async function login(email, password) { /* ... */ }export async function logout(sessionId) { /* ... */ }export function getCurrentUser() { /* ... */ }3. Avoid Circular Dependencies
Module A imports from Module B which imports from Module A:
import { B } from "./b.js";export const A = "A";
// b.jsimport { A } from "./a.js"; // ❌ Circular!export const B = "B";Refactor to extract shared dependencies into a third module.
Quick Reference
| Syntax | Description |
|---|---|
export const X = ... | Named export |
export default X | Default export |
import { X } from "./file.js" | Named import |
import X from "./file.js" | Default import |
import * as X from "./file.js" | Namespace import |
import("./file.js") | Dynamic import (Promise) |
export { X } from "./file.js" | Re-export |
import { X as Y } from "./file.js" | Aliased import |
Practice Exercises
Create a math library module: Export
add,subtract,multiply,divide, and a default exportcreateCalculator(). Import and use them in another file.Barrel exports: Create a
shapes/directory withcircle.js,rectangle.js, andtriangle.js. Each exports area/perimeter functions. Create anindex.jsbarrel that re-exports all of them.Dynamic import for i18n: Write a
setLanguage(lang)function that dynamically imports./i18n/en.js,./i18n/fr.js, or./i18n/es.jsbased on the parameter and calls atranslate(key)function from the loaded module.