# ES6 Modules: Complete Guide ## Introduction ES6 modules (ECMAScript modules) are the official standard for JavaScript modules, providing a clean and efficient way to organize and share code between different parts of an application. They offer static analysis, tree-shaking capabilities, and better performance compared to previous module systems. ### Key Benefits - **Static Analysis**: Import/export relationships are determined at compile time - **Tree Shaking**: Unused code can be eliminated during bundling - **Live Bindings**: Exported values maintain references to their original variables - **Strict Mode**: All modules run in strict mode by default - **Top-level Await**: Support for await at the module level ## Module Fundamentals ### What is a Module? A module is a file containing JavaScript code that can export values and import values from other modules. Each module has its own scope and can only access what it explicitly imports. ### Module Scope ```javascript // math.js - A simple module const PI = 3.14159 function circleArea(radius) { return PI * radius * radius } function circleCircumference(radius) { return 2 * PI * radius } // Only exported values are accessible from outside export { circleArea, circleCircumference } export const PI_VALUE = PI ``` ### Module Loading ```html <!-- Loading a module in HTML --> <script type="module" src="main.js"></script> ``` ```javascript // In Node.js with package.json { "type": "module" } ``` ## Export Syntax ### Named Exports Named exports allow you to export multiple values from a module: ```javascript // Exporting individual declarations export const name = 'square' export function draw(ctx, length, x, y, color) { ctx.fillStyle = color ctx.fillRect(x, y, length, length) return { length, x, y, color } } // Exporting a list const area = (length) => length * length const perimeter = (length) => 4 * length export { area, perimeter } // Renaming exports export { area as calculateArea, perimeter as calculatePerimeter } ``` ### Default Exports Each module can have only one default export: ```javascript // Default export of a function export default function randomSquare() { return Math.floor(Math.random() * 100); } // Default export of a class export default class Square { constructor(length) { this.length = length; } area() { return this.length * this.length; } } // Default export of a value const DEFAULT_CONFIG = { color: "blue", size: 100 }; export default DEFAULT_CONFIG; ``` ### Mixed Exports You can combine named and default exports: ```javascript // math-utils.js export const PI = 3.14159 export function circleArea(radius) { return PI * radius * radius } export default function calculateArea(shape, ...args) { switch (shape) { case 'circle': return circleArea(args[0]) case 'square': return args[0] * args[0] default: return 0 } } ``` ### Re-exporting You can re-export values from other modules: ```javascript // aggregator.js export { Square } from './shapes/square.js' export { Circle } from './shapes/circle.js' export { Triangle } from './shapes/triangle.js' // Re-exporting with renaming export { Square as SquareShape } from './shapes/square.js' // Re-exporting all exports export * from './utils/math.js' ``` ## Import Syntax ### Named Imports ```javascript // Import specific named exports import { name, draw, reportArea, reportPerimeter } from './modules/square.js' // Import with renaming import { name as squareName, draw as drawSquare } from './modules/square.js' // Import multiple with mixed renaming import { name as squareName, draw as drawSquare, reportArea as reportSquareArea } from './modules/square.js' ``` ### Default Imports ```javascript // Import default export import randomSquare from './modules/square.js' // Import default with renaming import { default as randomSquare } from './modules/square.js' // Import default and named exports together import randomSquare, { name, draw } from './modules/square.js' ``` ### Namespace Imports ```javascript // Import all exports as a namespace object import * as Square from './modules/square.js' // Usage Square.draw(ctx, 50, 50, 100, 'blue') Square.reportArea(100, reportList) ``` ### Side-effect Imports ```javascript // Import module for side effects only import './modules/initialize.js' ``` ## Module Types ### JavaScript Modules Standard JavaScript modules with `.js` extension: ```javascript // calculator.js export function add(a, b) { return a + b } export function multiply(a, b) { return a * b } ``` ### JSON Modules Import JSON files as modules: ```javascript // config.json { "apiUrl": "https://api.example.com", "timeout": 5000 } ``` ```javascript // main.js import config from './config.json' with { type: 'json' } console.log(config.apiUrl) ``` ### CSS Modules Import CSS files as modules: ```javascript import styles from './styles.css' with { type: 'css' } document.adoptedStyleSheets = [styles] ``` ## Dynamic Imports Dynamic imports allow you to load modules asynchronously at runtime: ### Basic Dynamic Import ```javascript // Dynamic import returns a Promise import('./modules/myModule.js').then((module) => { module.doSomething() }) // Using async/await async function loadModule() { const module = await import('./modules/myModule.js') module.doSomething() } ``` ### Conditional Loading ```javascript // Load different modules based on conditions async function loadModule() { let module if (typeof window === 'undefined') { // Server-side module = await import('./modules/server.js') } else { // Client-side module = await import('./modules/client.js') } return module } ``` ### Dynamic Module Paths ```javascript // Load multiple modules dynamically const modulePaths = [ './modules/module1.js', './modules/module2.js', './modules/module3.js' ] const modules = await Promise.all(modulePaths.map((path) => import(path))) modules.forEach((module) => { if (module.initialize) { module.initialize() } }) ``` ## Module Resolution ### Relative Paths ```javascript // Relative to current file import { helper } from './utils/helper.js' import { config } from '../config/settings.js' ``` ### Absolute URLs ```javascript // HTTP URLs import { library } from 'https://cdn.example.com/library.js' // Data URLs import data from 'data:text/javascript,export default 42;' ``` ### Bare Specifiers ```javascript // In Node.js or with import maps import _ from 'lodash' import { Component } from 'react' ``` ## Import Maps Import maps allow you to control how module specifiers are resolved: ### Basic Import Map ```html <script type="importmap"> { "imports": { "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js", "react": "https://cdn.jsdelivr.net/npm/react@18/index.js" } } </script> <script type="module"> import _ from 'lodash' import React from 'react' </script> ``` ### Scoped Import Maps ```html <script type="importmap"> { "imports": { "components/": "./src/components/", "utils/": "./src/utils/" } } </script> <script type="module"> import { Button } from 'components/Button.js' import { helper } from 'utils/helper.js' </script> ``` ## Advanced Features ### Top-level Await ```javascript // main.js const data = await fetch('/api/data').then((r) => r.json()) const processedData = await processData(data) export { processedData } ``` ### Live Bindings ```javascript // counter.js export let count = 0 export function increment() { count++ } // main.js import { count, increment } from './counter.js' console.log(count) // 0 increment() console.log(count) // 1 - reflects the change ``` ### Cyclic Dependencies ```javascript // a.js import { b } from './b.js' export const a = 'a' // b.js import { a } from './a.js' export const b = 'b' // This works because of hoisting ``` ### Import Attributes ```javascript // Import with attributes import data from './data.json' with { type: 'json' } import styles from './styles.css' with { type: 'css' } ``` ## Best Practices ### File Organization ``` src/ ├── components/ │ ├── Button.js │ └── Modal.js ├── utils/ │ ├── math.js │ └── validation.js ├── services/ │ └── api.js └── main.js ``` ### Naming Conventions ```javascript // Use descriptive names for exports export const API_BASE_URL = 'https://api.example.com' export function calculateTotalPrice(items) { /* ... */ } // Use consistent naming patterns export { Button as PrimaryButton } from './Button.js' export { Button as SecondaryButton } from './Button.js' ``` ### Error Handling ```javascript // Handle import errors gracefully try { const module = await import('./optional-module.js') module.initialize() } catch (error) { console.warn('Optional module not available:', error.message) } ``` ## Common Patterns ### Barrel Exports ```javascript // index.js - Barrel file export { Button } from './Button.js' export { Modal } from './Modal.js' export { Input } from './Input.js' // Usage import { Button, Modal, Input } from './components' ``` ### Factory Pattern ```javascript // factory.js export function createComponent(type, props) { switch (type) { case 'button': return new Button(props) case 'modal': return new Modal(props) default: throw new Error(`Unknown component type: ${type}`) } } ``` ### Plugin System ```javascript // plugin-manager.js const plugins = new Map() export function registerPlugin(name, plugin) { plugins.set(name, plugin) } export function getPlugin(name) { return plugins.get(name) } // plugin.js import { registerPlugin } from './plugin-manager.js' registerPlugin('myPlugin', { initialize() { console.log('Plugin initialized') } }) ``` ## Troubleshooting ### Common Errors #### "Cannot use import statement outside a module" ```html <!-- Solution: Add type="module" --> <script type="module" src="main.js"></script> ``` #### "Module not found" ```javascript // Ensure file extensions are included import { helper } from './utils/helper.js' // ✅ Correct import { helper } from './utils/helper' // ❌ Missing extension ``` #### "Circular dependency detected" ```javascript // Break circular dependencies by restructuring // or using dynamic imports const { dependency } = await import('./dependency.js') ``` ### Debugging Tips ```javascript // Check if module loaded correctly import * as module from './module.js' console.log(module) // Inspect module object // Use import.meta for debugging console.log(import.meta.url) // Current module URL console.log(import.meta.resolve('./utils.js')) // Resolved path ``` ## Module Architecture Diagram ```mermaid graph TB subgraph "Application Structure" A[main.js] --> B[components/] A --> C[utils/] A --> D[services/] B --> E[Button.js] B --> F[Modal.js] B --> G[Input.js] C --> H[math.js] C --> I[validation.js] D --> J[api.js] D --> K[auth.js] end subgraph "Module Types" L[JavaScript Modules] M[JSON Modules] N[CSS Modules] end subgraph "Import Methods" O[Static Import] P[Dynamic Import] Q[Namespace Import] end A -.-> L A -.-> M A -.-> N A -.-> O A -.-> P A -.-> Q style A fill:#ff4757,stroke:#2f3542,stroke-width:4px,color:#fff style B fill:#ff6348,stroke:#2f3542,stroke-width:3px,color:#fff style C fill:#ffa502,stroke:#2f3542,stroke-width:3px,color:#fff style D fill:#ff7675,stroke:#2f3542,stroke-width:3px,color:#fff style E fill:#fd79a8,stroke:#2f3542,stroke-width:2px,color:#fff style F fill:#fdcb6e,stroke:#2f3542,stroke-width:2px,color:#fff style G fill:#e17055,stroke:#2f3542,stroke-width:2px,color:#fff style H fill:#00b894,stroke:#2f3542,stroke-width:2px,color:#fff style I fill:#00cec9,stroke:#2f3542,stroke-width:2px,color:#fff style J fill:#6c5ce7,stroke:#2f3542,stroke-width:2px,color:#fff style K fill:#a29bfe,stroke:#2f3542,stroke-width:2px,color:#fff style L fill:#00b894,stroke:#2f3542,stroke-width:3px,color:#fff style M fill:#00cec9,stroke:#2f3542,stroke-width:3px,color:#fff style N fill:#55a3ff,stroke:#2f3542,stroke-width:3px,color:#fff style O fill:#fdcb6e,stroke:#2f3542,stroke-width:3px,color:#2f3542 style P fill:#e84393,stroke:#2f3542,stroke-width:3px,color:#fff style Q fill:#74b9ff,stroke:#2f3542,stroke-width:3px,color:#fff ``` ## Export/Import Flow Diagram ```mermaid flowchart LR subgraph "Module A" A1[Export Declaration] A2[Named Export] A3[Default Export] end subgraph "Module B" B1[Import Statement] B2[Named Import] B3[Default Import] B4[Namespace Import] end A1 --> B1 A2 --> B2 A3 --> B3 A1 --> B4 A2 --> B4 style A1 fill:#ff4757,stroke:#2f3542,stroke-width:3px,color:#fff style A2 fill:#00b894,stroke:#2f3542,stroke-width:3px,color:#fff style A3 fill:#6c5ce7,stroke:#2f3542,stroke-width:3px,color:#fff style B1 fill:#fdcb6e,stroke:#2f3542,stroke-width:3px,color:#2f3542 style B2 fill:#e84393,stroke:#2f3542,stroke-width:3px,color:#fff style B3 fill:#00cec9,stroke:#2f3542,stroke-width:3px,color:#fff style B4 fill:#74b9ff,stroke:#2f3542,stroke-width:3px,color:#fff ``` ## Dynamic Import Process ```mermaid sequenceDiagram participant M as Main Module participant R as Runtime participant T as Target Module M->>R: import('./module.js') R->>R: Parse module specifier R->>T: Fetch module file T->>R: Return module code R->>R: Compile and instantiate R->>M: Return module object M->>M: Use module exports Note over M,T: Dynamic imports are asynchronous<br/>and return Promises style M fill:#ff4757,stroke:#2f3542,stroke-width:3px,color:#fff style R fill:#00b894,stroke:#2f3542,stroke-width:3px,color:#fff style T fill:#6c5ce7,stroke:#2f3542,stroke-width:3px,color:#fff ``` ## Module Resolution Strategy ```mermaid graph TD A[Module Specifier] --> B{Type?} B -->|Relative| C[./module.js] B -->|Absolute| D[https://example.com/module.js] B -->|Bare| E[lodash] C --> F[Resolve relative to current file] D --> G[Use as-is] E --> H{Import Map?} H -->|Yes| I[Look up in import map] H -->|No| J[Node.js resolution] F --> K[Load Module] G --> K I --> K J --> K K --> L[Parse and Compile] L --> M[Execute Module] M --> N[Return Exports] style A fill:#ff4757,stroke:#2f3542,stroke-width:4px,color:#fff style B fill:#fdcb6e,stroke:#2f3542,stroke-width:3px,color:#2f3542 style C fill:#e17055,stroke:#2f3542,stroke-width:2px,color:#fff style D fill:#fd79a8,stroke:#2f3542,stroke-width:2px,color:#fff style E fill:#a29bfe,stroke:#2f3542,stroke-width:2px,color:#fff style F fill:#00b894,stroke:#2f3542,stroke-width:2px,color:#fff style G fill:#00cec9,stroke:#2f3542,stroke-width:2px,color:#fff style H fill:#fdcb6e,stroke:#2f3542,stroke-width:2px,color:#2f3542 style I fill:#e84393,stroke:#2f3542,stroke-width:2px,color:#fff style J fill:#74b9ff,stroke:#2f3542,stroke-width:2px,color:#fff style K fill:#00b894,stroke:#2f3542,stroke-width:3px,color:#fff style L fill:#6c5ce7,stroke:#2f3542,stroke-width:2px,color:#fff style M fill:#55a3ff,stroke:#2f3542,stroke-width:2px,color:#fff style N fill:#00cec9,stroke:#2f3542,stroke-width:3px,color:#fff ``` ## Conclusion ES6 modules provide a powerful and standardized way to organize JavaScript code. They offer static analysis, tree-shaking capabilities, and better performance compared to previous module systems. Understanding the various import/export syntaxes, dynamic imports, and module resolution strategies will help you build more maintainable and efficient JavaScript applications. Key takeaways: - Use named exports for multiple values, default exports for single primary values - Leverage dynamic imports for code splitting and conditional loading - Organize modules logically with clear naming conventions - Use import maps for better module resolution control - Handle errors gracefully when working with dynamic imports - Take advantage of live bindings for reactive programming patterns By following these patterns and best practices, you can create modular, maintainable, and performant JavaScript applications using ES6 modules.