Skip to main content

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:

  1. Export - How you make functions, objects, or values available to other modules
  2. 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:

javascript
// 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:

javascript
// 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

javascript
// 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:

javascript
// 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

javascript
// app.js
import greet from './greeting.js';

console.log(greet('Alice')); // Output: Hello, Alice!

Importing Both Default and Named Exports

javascript
// 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}`;
}
}
javascript
// 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

javascript
// 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:

javascript
// 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.

javascript
// counter.js
console.log('Counter module is initializing');
let count = 0;

export function increment() {
count++;
return count;
}

export function getCount() {
return count;
}
javascript
// 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:

javascript
// moduleA.js
const message = "Hello from module A";
console.log(message); // Output: Hello from module A
javascript
// 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:

javascript
// 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;
}
}
javascript
// 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();
}
javascript
// 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);
});
}
javascript
// 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);
});
html
<!-- 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 todos
  • todo-list.js - Manages the collection of todos
  • todo-renderer.js - Handles DOM manipulation and rendering
  • app.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:

html
<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:

  1. Backward compatibility - Supporting older browsers that don't understand ES modules
  2. Performance optimization - Bundling multiple modules into fewer files to reduce HTTP requests
  3. Code transformation - Processing non-JavaScript assets (CSS, images) as modules
  4. Development features - Hot module replacement, source maps, etc.

Here's how you'd typically use a module bundler in a project:

  1. Install the bundler (e.g., Webpack) via npm
  2. Create a configuration file that defines entry points and output settings
  3. 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

  1. Create a simple calculator module with add, subtract, multiply, and divide functions, then import and use it in another file.
  2. Convert an existing script to use modules by separating concerns into different files.
  3. Create a module that exports both a default export and several named exports, then practice different ways of importing them.
  4. Experiment with dynamic imports by loading a module only after a user interaction (like clicking a button).
  5. Create a small application that uses at least three modules working together.

Additional Resources

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! :)