# 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.