JavaScript ES modules: import and export explained

JavaScript ES modules — the native import / export syntax standardised in ES2015 — are now supported in all modern browsers and in Node.js, and they have replaced older module systems (AMD, CommonJS, IIFE globals) as the standard way to organise JavaScript code. Modules let you split your code into focused files, each with an explicit public API, and import only what you need. This eliminates global namespace pollution, makes dependencies explicit and traceable, and enables tree-shaking in bundlers like webpack and Rollup to remove unused code from production builds. In a WordPress context, you can load a JavaScript module directly with <script type="module">, which WordPress supports via the wp_enqueue_script_module() function introduced in WordPress 6.5, or by setting type=module through the script_loader_tag filter in older versions. Modules are always deferred by default — they never block rendering — and they run in strict mode automatically. They are also scoped to the file, so variables declared at the top level of a module are not added to the global window object. Combining modules with the async/await pattern and the Fetch API creates a clean, maintainable async architecture. The examples below cover named exports, default exports, re-exports, and dynamic imports.

Problem: You want to split your JavaScript into multiple files with clear dependencies, avoid global variable conflicts, and enable tree-shaking in your build process.

Solution: Use ES module syntax as shown in the examples below:

// utils.js — named exports
export function formatPrice(amount, currency = 'USD') {
    return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}

export function debounce(fn, delay) {
    let timer;
    return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
}

export const API_BASE = '/wp-json/wc/v3';

// cart.js — default export
import { formatPrice, API_BASE } from './utils.js';

export default class Cart {
    constructor() {
        this.items = [];
    }

    async fetchItems() {
        const response = await fetch(`${API_BASE}/cart`);
        this.items = await response.json();
        return this.items;
    }

    getTotal() {
        return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
    }

    render() {
        return formatPrice(this.getTotal());
    }
}

// main.js — import and use
import Cart from './cart.js';
import { debounce } from './utils.js';

const cart = new Cart();
cart.fetchItems().then(() => console.log('Total:', cart.render()));

// Dynamic import — load module only when needed
document.querySelector('#open-gallery').addEventListener('click', async () => {
    const { initGallery } = await import('./gallery.js');
    initGallery('#gallery-container');
});

// WordPress: load as module via script_loader_tag filter
// add_filter('script_loader_tag', function($tag, $handle) {
//     if ($handle === 'my-module') {
//         return str_replace('