JavaScript Modules
Modern JavaScript applications can grow quickly in size and complexity. As applications scale, organizing your code becomes increasingly important. JavaScript modules provide a way to split your code into separate files, making it more maintainable, reusable, and easier to understand.
What are JavaScript Modules?
A module is a self-contained piece of code that provides specific functionality to your application. Modules allow you to:
- Split large code files into smaller, manageable pieces
- Encapsulate code, keeping variables and functions private
- Explicitly import code you need and export code you want to share
- Manage dependencies more effectively
- Avoid polluting the global namespace
Before modules became standard in JavaScript, developers had to use patterns like Immediately Invoked Function Expressions (IIFEs) or library-specific module systems like CommonJS or AMD. Now, with ES6 modules (also called ES modules or ESM), we have a standardized way to organize our code.
Module Basics: Export and Import
The two key concepts to understand with JavaScript modules are:
- Export - How you make functions, objects, or values available to other modules
- Import - How you access functionality from other modules
Let's explore how these work.
Exporting from a Module
You can export values from a module using the export
keyword. There are two main types of exports:
Named Exports
Named exports allow you to export multiple values from a module:
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// You can also define first, export later
const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;
export { multiply, divide };
Default Exports
Each module can have one default export, which is often used when a module primarily provides a single functionality:
// greeting.js
function greet(name) {
return `Hello, ${name}!`;
}
export default greet;
Importing from a Module
To use the exported values from another module, you use the import
keyword:
Importing Named Exports
// app.js
import { PI, add, subtract } from './math.js';
console.log(PI); // Output: 3.14159
console.log(add(5, 3)); // Output: 8
console.log(subtract(10, 4)); // Output: 6
You can also rename imports to avoid naming conflicts:
// app.js
import { add as sum, multiply } from './math.js';
console.log(sum(5, 3)); // Output: 8
console.log(multiply(2, 4)); // Output: 8
Importing Default Exports
// app.js
import greet from './greeting.js';
console.log(greet('Alice')); // Output: Hello, Alice!
Importing Both Default and Named Exports
// user.js
export const userRole = 'admin';
export const isAuthenticated = true;
export default class User {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hi, I'm ${this.name}`;
}
}
// app.js
import User, { userRole, isAuthenticated } from './user.js';
const admin = new User('Bob');
console.log(admin.sayHello()); // Output: Hi, I'm Bob
console.log(userRole); // Output: admin
Importing All Exports as an Object
// app.js
import * as MathUtils from './math.js';
console.log(MathUtils.PI); // Output: 3.14159
console.log(MathUtils.add(7, 3)); // Output: 10
console.log(MathUtils.multiply(2, 5)); // Output: 10
Dynamic Imports
Sometimes you may want to load a module conditionally or on-demand rather than upfront. Dynamic imports allow you to load modules asynchronously:
// app.js
const loadModule = async () => {
if (userLoggedIn) {
// Load the module dynamically
const { default: UserDashboard } = await import('./userDashboard.js');
return new UserDashboard();
} else {
const { default: LoginPage } = await import('./loginPage.js');
return new LoginPage();
}
};
// Dynamic imports return a promise
loadModule().then(component => {
component.render();
});
Module Features and Behavior
Modules are Always in Strict Mode
All modules execute in strict mode by default, which helps catch common coding mistakes and prevents certain unsafe actions.
Modules Execute Only Once
No matter how many times a module is imported, it only executes once. Subsequent imports reference the same instance of the module.
// counter.js
console.log('Counter module is initializing');
let count = 0;
export function increment() {
count++;
return count;
}
export function getCount() {
return count;
}
// app.js
import { increment, getCount } from './counter.js';
import { getCount as getCountAgain } from './counter.js';
console.log(increment()); // Output: 1
console.log(increment()); // Output: 2
console.log(getCount()); // Output: 2
console.log(getCountAgain()); // Output: 2 (same instance)
// Console shows 'Counter module is initializing' only once
Module Scope
Variables defined in a module are scoped to that module and won't pollute the global scope:
// moduleA.js
const message = "Hello from module A";
console.log(message); // Output: Hello from module A
// moduleB.js
console.log(message); // Error: message is not defined
Real-World Example: Building a Simple To-Do Application
Let's see how modules can be used in a practical application:
// todo-item.js
export default class TodoItem {
constructor(text, completed = false) {
this.id = Date.now();
this.text = text;
this.completed = completed;
}
toggle() {
this.completed = !this.completed;
}
}
// todo-list.js
import TodoItem from './todo-item.js';
export default class TodoList {
constructor() {
this.items = [];
}
addItem(text) {
const newItem = new TodoItem(text);
this.items.push(newItem);
return newItem;
}
removeItem(id) {
this.items = this.items.filter(item => item.id !== id);
}
getItems() {
return this.items;
}
}
export function createTodoList() {
return new TodoList();
}
// todo-renderer.js
export function renderTodoItem(item, onToggle, onDelete) {
const li = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = item.completed;
checkbox.addEventListener('change', () => onToggle(item.id));
const text = document.createElement('span');
text.textContent = item.text;
if (item.completed) {
text.style.textDecoration = 'line-through';
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => onDelete(item.id));
li.appendChild(checkbox);
li.appendChild(text);
li.appendChild(deleteBtn);
return li;
}
export function renderTodoList(todoList, container) {
container.innerHTML = '';
todoList.getItems().forEach(item => {
const toggleItem = (id) => {
const item = todoList.getItems().find(item => item.id === id);
item.toggle();
renderTodoList(todoList, container);
};
const deleteItem = (id) => {
todoList.removeItem(id);
renderTodoList(todoList, container);
};
const itemElement = renderTodoItem(item, toggleItem, deleteItem);
container.appendChild(itemElement);
});
}
// app.js
import { createTodoList } from './todo-list.js';
import { renderTodoList } from './todo-renderer.js';
document.addEventListener('DOMContentLoaded', () => {
const todoList = createTodoList();
const container = document.getElementById('todo-container');
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value.trim()) {
todoList.addItem(input.value.trim());
input.value = '';
renderTodoList(todoList, container);
}
});
// Initialize empty list
renderTodoList(todoList, container);
});
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo App</title>
<!-- Note the type="module" attribute -->
<script type="module" src="app.js"></script>
</head>
<body>
<h1>Todo List</h1>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="Add new todo">
<button type="submit">Add</button>
</form>
<ul id="todo-container"></ul>
</body>
</html>
In this example, we've split our todo application into multiple modules:
todo-item.js
- Handles the data structure for individual todostodo-list.js
- Manages the collection of todostodo-renderer.js
- Handles DOM manipulation and renderingapp.js
- Main application logic
Each module has a specific responsibility, making the code easier to understand, test, and maintain.
Using JavaScript Modules in the Browser
To use JavaScript modules directly in the browser, you need to specify type="module"
in your script tag:
<script type="module" src="app.js"></script>
Important notes about modules in browsers:
- Module scripts are automatically deferred (they load after HTML parsing finishes)
- They follow CORS rules (can't load from different origins without proper headers)
- They need to be served over HTTP(S), not via file:// URLs
- They run in strict mode by default
Module Bundlers
While modern browsers support JavaScript modules natively, many projects still use module bundlers like Webpack, Rollup, or Parcel for:
- Backward compatibility - Supporting older browsers that don't understand ES modules
- Performance optimization - Bundling multiple modules into fewer files to reduce HTTP requests
- Code transformation - Processing non-JavaScript assets (CSS, images) as modules
- Development features - Hot module replacement, source maps, etc.
Here's how you'd typically use a module bundler in a project:
- Install the bundler (e.g., Webpack) via npm
- Create a configuration file that defines entry points and output settings
- Run the bundler to create production-ready bundled JavaScript
Summary
JavaScript modules are a powerful feature that helps you organize your code into manageable, reusable pieces. They provide better encapsulation, dependency management, and code organization through a standardized import and export system.
Key takeaways:
- Use
export
to make values available to other modules - Use
import
to access functionality from other modules - Modules execute only once, regardless of how many times they're imported
- Modules run in strict mode by default
- Variables in modules are scoped to that module
- Use
type="module"
to use modules directly in browsers - Consider using module bundlers for production applications
Exercises
- Create a simple calculator module with add, subtract, multiply, and divide functions, then import and use it in another file.
- Convert an existing script to use modules by separating concerns into different files.
- Create a module that exports both a default export and several named exports, then practice different ways of importing them.
- Experiment with dynamic imports by loading a module only after a user interaction (like clicking a button).
- Create a small application that uses at least three modules working together.
Additional Resources
- MDN Web Docs: JavaScript Modules
- JavaScript.info: Modules
- ES6 In Depth: Modules
- Exploring JS: Modules
Happy coding with JavaScript modules!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)