W3docs

JavaScript Modules Introduction

JavaScript modules help you manage large code projects by letting you split your code into separate, reusable parts. This guide dives into how to use JavaScript

JavaScript modules help you manage large code projects by letting you split your code into separate, reusable parts. This guide dives into how to use JavaScript modules effectively, enhancing your programming skills and project organization.

This page covers what a module is, the import/export syntax, the difference between named and default exports, how module scope works, the older module systems you will still encounter (CommonJS and AMD), and a complete worked example you can run.

Understanding JavaScript Modules

A module is simply a file. Each file gets its own private scope: variables and functions you declare inside a module are not visible outside it unless you explicitly export them. To use something from another module, you import it. This is the foundation of every other idea on this page.

Modules also bring two automatic behaviors you should know about:

  • Strict mode is always on. Every module runs as if it started with 'use strict', so accidental globals throw a ReferenceError instead of silently creating a variable.
  • Top-level this is undefined, not the global object. This is one of the easiest ways to tell whether code is running as a module.

Below, we cover the basic syntax and use of modules.

Introduction to Module Syntax

Modules use import and export statements to share code between files.

Example:

// Exporting functions
export const add = (a, b) => a + b;
export function multiply(a, b) {
  return a * b;
}

// Importing them in another file
import { add, multiply } from './mathFunctions.js';

The export statement lets you make parts of your module available to other files. The import statement lets you bring in those parts where you need them.

Info

Use clear, descriptive names for your modules and functions. This makes your code easier to read and maintain.

Default Exports vs Named Exports

You can use default or named exports to share different parts of your code. Named exports share multiple features by name, and default exports share a single feature without specifying a name.

Example:

// mathFunctions.js
export default function subtract(a, b) {
  return a - b;
}

// Using the default export
import subtract from './mathFunctions.js';

The default keyword is used to export a single function or variable. When importing a default export, you can use any name to refer to it.

Info

Named exports are good for utility functions, and default exports are useful for the single main feature of a file, like a React component or a class.

A file can mix one default export with any number of named exports. You import them together, the default first and the named ones in braces:

// mathFunctions.js
export const add = (a, b) => a + b;        // named export
export default function subtract(a, b) {    // default export
  return a - b;
}

// app.js
import subtract, { add } from './mathFunctions.js';

A few rules worth remembering:

  • A module can have only one default export, but many named exports.
  • Named imports must use the exact exported name (or rename with as: import { add as sum } from './mathFunctions.js').
  • A default import can use any name you like, because the default has no fixed name.

Renaming and Re-exporting

You can rename on the way in or out with as, and re-export from another module to build a single entry point (a "barrel" file):

// utils.js
export { add as sum } from './mathFunctions.js';
export { default as subtract } from './mathFunctions.js';

// app.js
import { sum, subtract } from './utils.js';

Leveraging Modules for Clean Code

Using modules can make your code easier to handle, especially as projects grow larger.

Directory Structure

A good folder setup helps keep your code organized.

Example:

/src
  /components
  /helpers
  /models
  /services
index.js

Handling Dependencies

Managing dependencies means making sure your code files work together correctly.

Example:

// Webpack configuration for bundling modules
const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' }
    ]
  }
};

This configuration tells Webpack to start with index.js, bundle it and all its dependencies into bundle.js, and put that in the dist folder.

Info

Note: Webpack configuration files traditionally use CommonJS (module.exports) syntax, even in ES6-heavy projects.

Advanced Module Techniques

Using advanced module features can make your code more efficient and easier to manage.

Dynamic Imports

Dynamic imports let you load code only when it's needed, which can speed up your application.

Example:

// Dynamically importing a module
// Assuming a button element exists
button.onclick = () => {
  import('./messageModule.js')
    .then(module => {
      module.showMessage('Dynamic Import Executed!');
    });
};

This code loads a module only when a button is clicked, which can reduce initial load times.

Info

Use dynamic imports for parts of your app that aren't immediately necessary, like additional features accessed later.

import() returns a promise, so you can also use it with async/await:

async function loadFeature() {
  const module = await import('./messageModule.js');
  module.showMessage('Loaded on demand');
}

See Dynamic Imports for a deeper look.

Cross-Module Communication

Modules should be self-contained but can communicate through shared resources.

Example:

// stateManager.js
export let state = { count: 0 };

// counter.js
import { state } from './stateManager.js';
state.count++;

This code shows two modules sharing a state object. Because JavaScript modules are cached as singletons, both files reference the exact same object in memory. When one module changes the state, the other sees the change.

Info

Note: This example mutates the shared object directly. For production apps, consider using a state management library or a reactive pattern to handle updates predictably.

Understanding Module Systems and Their Usage Today

In modern web development, understanding the various module systems is essential:

  • CommonJS: Primarily used in Node.js for server-side code.
  • AMD (Asynchronous Module Definition): Used for asynchronous loading of modules, suitable for browsers.
  • ES6 Modules: The standard in modern web development, supporting both synchronous and asynchronous loading.

Examples for Each Module System

To illustrate these concepts in JavaScript, we will include snippets for each module type alongside the existing ES6 example.

CommonJS Example:

// mathFunctions.js
exports.add = function(a, b) {
  return a + b;
};

// app.js
const math = require('./mathFunctions.js');
console.log(math.add(5, 3));

Explanation for CommonJS: In this example, we use exports to make the add function available outside the file. Then, in another file, we use require to bring in the add function so we can use it. This system is commonly used in Node.js.

AMD Example:

// mathFunctions.js
define([], function() {
  return {
    add: function(a, b) {
      return a + b;
    }
  };
});

// app.js
require(['mathFunctions'], function(math) {
  console.log(math.add(5, 3));
});

Explanation for AMD: This example uses define to declare a module with no dependencies and returns an object containing the add function. require is then used to load the module asynchronously. This is helpful for loading modules dynamically in the browser.

ES6 Modules Example:

// mathFunctions.js
export const add = (a, b) => a + b;

// app.js
import { add } from './mathFunctions.js';
console.log(add(5, 3));

Explanation for ES6 Modules: Here, we use export to make the add function available, and import to use it in another file. This is the modern standard for handling modules in JavaScript and is supported by most browsers.

Enabling ES6 Modules in Node.js

When using ES6 modules in Node.js, you can take advantage of the same import/export syntax that is typically used in front-end JavaScript development. This allows for a consistent module syntax across both client and server environments.

To use ES6 module syntax in Node.js, you need to ensure your environment supports it. As of Node.js version 14, ES6 modules are stable, and since version 16, they are enabled by default when "type": "module" is set. Here’s how you can set it up:

Update package.json: In your Node.js project’s package.json file, add the following line:

"type": "module"

This tells Node.js to treat .js files as ES6 modules by default.

File Extensions: Use .js for your module files, or explicitly use .mjs if you prefer. Node.js recognizes both, but if you're using the "type": "module" setting, .js will be assumed to be an ES6 module.

// mathFunctions.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './mathFunctions.js';
console.log(add(5, 3)); // 8
Info

In Node.js ESM, relative imports require the file extension (./mathFunctions.js, not ./mathFunctions). This differs from CommonJS, where the extension is optional.

Common Gotchas

A few module behaviors trip up newcomers. Knowing them up front saves debugging time:

  • Relative paths need an extension in the browser. import { add } from './math' works in many bundlers but fails with native browser ESM, where you must write './math.js'.
  • Modules run once and are cached. No matter how many files import the same module, its top-level code runs a single time and every importer shares the same instance. This is what made the shared-state example above work.
  • Imports are read-only bindings, not copies. You cannot reassign an imported value (add = 5 throws). You are looking at a live link to the export, so if the exporting module changes the value, importers see the new value.
  • import/export must be top-level. Static import and export statements cannot appear inside an if block or a function. When you need conditional loading, use the dynamic import() function instead.
  • Module scripts are deferred. A <script type="module"> in the browser does not block parsing; it runs after the HTML is parsed, similar to adding defer.

For more on the always-on strict mode, see Strict mode. For the full export/import reference, see Export and Import.

Best Practices for Using JavaScript Modules

  1. Keep It Simple: Use clear, simple names for your files and exports.
  2. Be Consistent: Apply the same patterns and structures across your project to make your code predictable.
  3. Document Everything: Comment your code and document how to use your modules.
  4. Optimize as Needed: Regularly review and optimize your code as your project grows.

Full Example

Below is a full example that integrates everything you've learned in this article.

Project Structure:

/src
  /math
    - mathFunctions.js
  - app.js
index.html

mathFunctions.js:

export const add = (a, b) => a + b;
export default function subtract(a, b) {
  return a - b;
}

app.js:

import subtract, { add } from './math/mathFunctions.js';

document.getElementById('add').addEventListener('click', function() {
  const result = add(5, 3);
  document.getElementById('result').textContent = `Adding: ${result}`;
});

document.getElementById('subtract').addEventListener('click', function() {
  const result = subtract(5, 3);
  document.getElementById('result').textContent = `Subtracting: ${result}`;
});

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>JavaScript Module Example</title>
</head>
<body>
  <button id="add">Add 5 + 3</button>
  <button id="subtract">Subtract 5 - 3</button>
  <div id="result"></div>
  <script type="module" src="src/app.js"></script>
</body>
</html>

Note: ES modules require a local development server (e.g., npx serve or Vite) to run in the browser due to CORS restrictions. Opening index.html directly via file:// will fail.

This setup uses a simple web page with buttons to demonstrate adding and subtracting numbers using imported functions. The results are displayed directly on the page.

Explanation of the Example

  • mathFunctions.js: This file contains two functions (add and subtract) which are exported as modules. add is a named export, and subtract is a default export.
  • app.js: This file imports the functions from mathFunctions.js and binds them to button click events to perform calculations when the user interacts with the page.
  • index.html: The HTML file sets up the user interface with buttons and a display area for results. It links to app.js as a module.

This full example demonstrates how JavaScript modules can be structured and used within a real application.

Conclusion

JavaScript modules are a powerful tool for organizing and maintaining large-scale web applications. By understanding and using different module systems appropriately, you can enhance your project’s scalability and maintainability. Regularly updating your knowledge of module syntax, best practices, and advanced techniques will ensure your development skills remain sharp and your projects stay ahead of the curve.

Practice

Practice
What are the benefits of using JavaScript modules?
What are the benefits of using JavaScript modules?
Was this page helpful?