Zum Inhalt springen

How to Use import/export in JavaScript ES6 Modules

As your JavaScript projects grow, keeping all code in one file becomes unmanageable. Before JavaScript ES6 Modules, JavaScript had no built-in way to organize code into separate files.

But ES6 (ECMAScript 2015) introduced native module support using export and import, which allows you to split code into multiple files and reuse it cleanly.

By using modules, you can:

  • Import functions, variables, or classes from other files.
  • Export only what you need.
  • Organize and reuse code more effectively.

In this post, you’ll learn how to use JavaScript ES6 Modules with import and export syntax. You’ll explore different ways to export and import code, understand module scope, handle dynamic imports, re-export modules, and apply best practices to write clean, modular, and maintainable JavaScript.

Before we get started, don’t forget to subscribe to my newsletter!
Get the latest tips, tools, and resources to level up your web development skills delivered straight to your inbox. Subscribe here!

Now let’s jump right into it!🚀

What are JavaScript ES6 Modules?

You can think of each JavaScript file like a warehouse. Where you use export to pack selected tools (functions, variables) into a box and use import in another file to receive and unpack those tools.

This keeps your code organized, reusable, and modular, just like sending only what you need from one warehouse to another.

Why Use Modules?

  • Cleaner code: Break large files into smaller parts.
  • Reusability: You can create reusable components that you write once and reuse anywhere.
  • Maintainability: The logic stays in separate files that make it easier to debug and update.
  • Native support: Works in modern browsers and Node.js.

👉 If you’re new to JavaScript, check out my beginner-friendly tutorials over on Learnify, my curated platform to help you learn JavaScript step by step, with examples and simple explanations.

How to Enable Modules

To enable ES6 modules, you must use a module-aware environment.

In HTML:

Add type="module" to your script.

<script type="module" src="app.js"></script>

In Node.js:

Add "type": "module" in package.json file.

{
  "type": "module"
}

Note:

  • If "type": "module" is set in package.json, use .js extension file for your modules.
  • If not using that, you must use .mjs extension file for your modules.

Exporting from a Module

JavaScript provides the following ways to export from a module.

Named Export

By using named export, you can export multiple values from a module.

// math.js
export const PI = 3.14;

export function add(a, b) {
  return a + b;
}

export const subtract = (a, b) => a - b;

Here,

  • export const PI = 3.14;:
    • export makes this variable available to be imported in another file.
    • const PI = 3.14; declares a constant named PI with a value of 3.14.
  • export function add(a, b) { return a + b;}:
    • export makes the function add available outside this file.
    • function add(a, b) declares a function that takes two arguments.
    • return a + b; returns the sum of a and b.
  • export const subtract = (a, b) => a - b;:
    • export makes subtract available to be imported.
    • const subtract = (a, b) => a - b; declares a function using arrow syntax that returns the result of a - b.

You can also group them like this:

const PI = 3.14;

function add(a, b) {
  return a + b;
}

const subtract = (a, b) => a - b;

export { PI, add, subtract };

Here, instead of exporting the variable (PI) and functions (add and subtract) as you declare them, you export them all together at the bottom using export { ... }.

Default Export

By using the default export, you can export one main thing per file.

Each module can have one default export, which can be a function, class, object, or primitive value.

// logger.js
export default function log(msg) {
  console.log(msg);
}

// Or with an object

// config.js
export default {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

Here,

  • export default function log(msg) {console.log(msg);}, exports the function log as the default export and you can rename it freely when importing.
  • export default {...}; exports a default object with config values.

Importing from a Module

Importing Named Exports

import { PI, add, subtract } from "./math.js";

console.log(PI); // Output: 3.14
console.log(add(5, 3)); // Output: 8
console.log(subtract(7, 7)); // Output: 0

Here, import { PI, add, subtract } from "./math.js"; import PI, add, and subtract from math.js. You can use these imported functions and constants wherever you need.

Importing Default Exports

You can import default exports with any name.

import log from "./logger.js";
import config from "./config.js";

log("App started");
console.log(config.apiUrl);

Here,

  • import log from "./logger.js"; imports the default export from logger.js and names it log.
  • import config from "./config.js"; imports the default export from config.js as config.

Importing Everything

import * as MathUtils from "./math.js";

console.log(MathUtils.PI); // Output: 3.14
console.log(MathUtils.add(5, 3)); // Output: 8
console.log(MathUtils.subtract(7, 7)); // Output: 0

Here,

  • import * as MathUtils from "./math.js"; imports all named exports from math.js under the namespace MathUtils.
  • You can access the exports via MathUtils prefix, like, console.log(MathUtils.PI);.

Selective Imports

You can import only what you need like this:

import { add } from "./math.js"; // Only imports add function

This only imports the add function from math.js.

Module Scope

In ES6 modules, variables, functions, and classes declared in a file are scoped to that module only. They’re not added to the global scope like traditional <script> tags.

// math.js
const secret = 42;
export const add = (a, b) => a + b;

// app.js
import { add } from "./math.js";

console.log(secret); // ReferenceError: secret is not defined

Only the things you explicitly export can be accessed in another file.

Renaming Exports and Imports

You can change the name when exporting like this:

function add(a, b) {
  return a + b;
}

export { add as sum };

Here, export { add as sum }; exports the function add as sum.

You can change the name when importing like this:

import { PI as pi } from "./math.js";

console.log(pi); // Output: 3.14

Here, you’re importing PI but renaming it to pi in this file.

Default vs Named: What’s the Difference?

  • You can think of named exports like putting labels on individual items in your box. Then, when importing, you must use the same names.
  • You can think of default export like marking one item as the main thing in the box. Then, when importing, you can use any name.
  • You can have many named exports.
  • You can only have one default export per file.
  • You can rename default exports freely when importing.

When to use default and named?

  • Use named exports when exporting multiple values.
  • Use default export when the file has one main thing to export.

Mixed Exports and Imports

Combining Named and Default Exports

You can also combine named and default exports in a file like this:

// config.js

export default {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
};

export const HTTP_METHODS = {
  GET: "GET",
  POST: "POST",
  PUT: "PUT",
  DELETE: "DELETE",
};

Importing Mixed Exports

You can import mixed exports like this:

import config, { HTTP_METHODS } from "./config.js";

Alternative Mixed Import Syntax

import config, * as ApiUtils from "./config.js";

console.log(config.apiUrl); // Output: https://api.example.com
console.log(ApiUtils.HTTP_METHODS.POST); // Output: POST

Here, config holds default and ApiUtils holds all named exports.

Dynamic Imports

Dynamic imports allow you to load modules only when you need them (conditionally and asynchronously at runtime).

Basic dynamic import:

// app.js
async function loadMathModule() {
  const mathModule = await import("./math.js");
  console.log(mathModule.add(5, 3));
}

// Or with .then()

import("./math.js")
  .then((mathModule) => {
    console.log(mathModule.PI);
  })
  .catch((error) => {
    console.error("Failed to load module:", error);
  });

Here,

  • async function loadMathModule() { ... }: Defines an asynchronous function so you can use await.
  • const mathModule = await import("./math.js");: Dynamically imports the module math.js at runtime and waits until it loads.
  • mathModule.add(5, 3): Accesses the add function exported from math.js and calls it with arguments 5 and 3.
  • console.log(...): Logs the result (8) to the console.
  • In the second snippet, dynamic import is used with .then() instead of await.
  • import("./math.js") returns a Promise that resolves to the module object.
  • .then((mathModule) => {...}) runs when the module loads successfully.
  • mathModule.PI accesses the exported constant PI from math.js.
  • .catch(...) handles any errors if the module fails to load.

Conditional module loading:

// app.js

async function loadFeature(featureName) {
  if (featureName === "advanced") {
    const { AdvancedCalculator } = await import("./advanced-calculator.js");
    return new AdvancedCalculator();
  } else {
    const BasicCalculator = await import("./basic-calculator.js");
    return new BasicCalculator.default();
  }
}

Here,

  • loadFeature loads a different module based on featureName.
  • If featureName is „advanced“, it imports AdvancedCalculator from "./advanced-calculator.js" using destructuring.
  • It then creates and returns a new instance of AdvancedCalculator.
  • Otherwise, it imports the default export from "./basic-calculator.js", which is accessed via .default.
  • Returns a new instance of BasicCalculator.
  • This shows how dynamic import lets you conditionally load code only when needed.

Dynamic import with destructuring:

// app.js

async function performCalculation() {
  const { PI, add, subtract } = await import("./math.js");

  const result = add(subtract(2, PI), 5);
  return result;
}

Here,

  • Dynamically imports math.js and uses destructuring to directly get PI, add, and subtract.
  • Then calls subtract(2, PI), subtracting PI from 2.
  • Passes that result to add(..., 5) to add 5.
  • Returns the final computed result.
  • This shows how you can cleanly access multiple named exports from a dynamic import.

Code splitting with dynamic imports:

// app.js

document.getElementById("load-chart").addEventListener("click", async () => {
  const { Chart } = await import("./chart-library.js");
  const chart = new Chart("#chart-container");
  chart.render();
});

Here,

  • Adds a click event listener on an element with id „load-chart“.
  • When clicked, dynamically imports the Chart class from "./chart-library.js".
  • Creates a new Chart instance targeting the DOM element #chart-container.
  • Calls chart.render() to display the chart.
  • This defers loading the potentially large chart library until the user actually needs it, improving initial load performance.

Re-Exporting Modules

Re-exporting allows you to gather exports from other files and share them from a single entry point, like combining tools from different toolboxes into one central box.

Simple Re-export:

// index.js (barrel export)

export { add, subtract } from "./math.js";
export { default as Logger } from "./logger.js";

Here,

  • export { add, subtract } from "./math.js"; re-exports the named exports add and subtract from math.js through index.js.
  • So anyone can now import them like, import { add } from './index.js';
  • export { default as Logger } from "./logger.js"; re-exports the default export from logger.js under the name Logger.
  • It allows import like, import { Logger } from './index.js';

Re-export with renaming:

// index.js

export { add as sum } from "./math.js";

export { default as Calculator } from "./calculator.js";

Here,

  • export { add as sum } from "./math.js"; renames the add function from math.js to sum during re-export.
  • You can use it like, import { sum } from './index.js';
  • export { default as Calculator } from "./calculator.js"; re-exports the default export from calculator.js under the name Calculator.

Re-export all named exports:

// index.js
export * from "./math.js"; // Re-exports all named exports
export * from "./utilities.js"; // Re-exports all named exports

Note: This does not re-export default exports. You still need to handle default exports explicitly.

Re-export all as namespace:

// index.js
export * as MathUtils from "./math.js";
export * as StringUtils from "./string-utils.js";

// Usage:
// import { MathUtils, StringUtils } from './index.js';

Here,

  • export * as MathUtils from "./math.js"; bundles all exports from math.js into an object called MathUtils.
  • export * as StringUtils from "./string-utils.js"; bundles all exports from string-utils.js into an object called StringUtils.

Complex re-export pattern:

// modules/index.js
export { default as Calculator } from "./calculator.js";
export { default as Logger } from "./logger.js";
export * from "./math.js";
export * from "./string-utils.js";
export { ApiClient as Client, HTTP_METHODS as Methods } from "./config.js";

Here,

  • export { default as Calculator } from "./calculator.js"; re-exports default export from calculator.js as Calculator.
  • export { default as Logger } from "./logger.js"; re-exports default export from logger.js as Logger.
  • export * from "./math.js"; re-exports all named exports from math.js.
  • export * from "./string-utils.js"; re-exports all named exports from string-utils.js.
  • export { ApiClient as Client, HTTP_METHODS as Methods } from "./config.js"; renames and re-exports ApiClient as Client and HTTP_METHODS as Methods from config.js.
  • This gives you a single entry point (modules/index.js) for many exports across your app, a common and clean practice for large codebases.

Module Loading Considerations

How Module Paths Work

// Relative imports:

import { add } from "./math.js"; // Same directory (./ means the same folder)
import { User } from "../models/user.js"; // Parent directory (../ means go up one folder)
import { config } from "./config/app.js"; // Subdirectory (Inside config/ folder)

// Absolute imports (with build tools):

// This kind of path works only if you're using a build tool (like Vite, Webpack, etc.) that supports absolute paths from a root folder (like /src).

import { Component } from "/src/components/component.js";

// Node modules:

// This imports built-in or installed modules (like express or fs) in Node.js.
// No need for ./ because they’re from node_modules or the standard library.

import express from "express";
import { readFile } from "fs/promises";

Which Modules Run First? (Execution Order)

// Modules execute in dependency order
// main.js
import "./setup.js"; // Executes first
import "./app.js"; // Executes second
import "./cleanup.js"; // Executes last

console.log("Main module loaded");
  • ES6 modules are executed in the order they’re imported.
  • Even if you don’t import any variables, the file runs for side effects (like setup or logging).
  • This is useful for initializing settings or running startup code.

Code That Runs Automatically (Side Effects)

// analytics.js
console.log("Analytics module loaded");

let pageViews = 0;

export function trackPageView() {
  pageViews++;
  console.log(`Page views: ${pageViews}`);
}

// This runs when module is first imported
window.addEventListener("load", () => {
  console.log("Page loaded - analytics ready");
});
  • console.log("Analytics module loaded"); runs immediately when the file is imported, not when a function is called.
  • This is a side effect, useful for logging, setup, or global listeners.
  • The event listener also runs when the page loads, which is useful for initialization logic.

When Files Import Each Other (Circular Dependencies)

// user.js
import { Post } from "./post.js";

export class User {
  constructor(name) {
    this.name = name;
    this.posts = [];
  }

  addPost(content) {
    this.posts.push(new Post(content, this));
  }
}

// post.js
import { User } from "./user.js";

export class Post {
  constructor(content, author) {
    this.content = content;
    this.author = author; // User instance
  }
}
  • user.js imports Post from post.js, and at the same time post.js imports User from user.js.
  • This is called a circular dependency.
  • It works if only class definitions are involved, but can cause errors if you try to run functions or access variables immediately during the import.
  • In complex apps, avoid circular dependencies or use dynamic imports to fix them.

Best Practices

  • Prefer named exports (Helps with Tree Shaking):

Tree shaking means removing unused code during the build to make your app smaller and faster.

Named exports help tools like Webpack or Vite remove what you don’t use. Because with named exports, bundlers know exactly what you’re using, and can skip what you’re not. This makes your final bundle smaller.

  // Good: Named exports are more explicit
  export function validateEmail(email) {
    /* ... */
  }
  export function validatePassword(password) {
    /* ... */
  }

  // Avoid: Default exports can be ambiguous
  export default {
    validateEmail,
    validatePassword,
  };
  • Use descriptive names:
  // Good: Clear naming
  import { validateEmail, validatePassword } from "./validators.js";

  // Avoid: Generic names
  import { check1, check2 } from "./validators.js";
  • Group related imports:

Group related modules into folders (e.g., /utils, /components) to keep your codebase organized.

  // Good: Organized imports
  import React, { useState, useEffect } from "react";
  import { Router, Route, Switch } from "react-router-dom";

  import { ApiClient } from "./api/client.js";
  import { validateForm } from "./utils/validation.js";
  import { logger } from "./utils/logger.js";

  import "./styles/app.css";
  • Avoid deep import paths:
  // Good: Use barrel exports
  import { UserService, PostService } from "./services/index.js";

  // Avoid: Deep nesting
  import { UserService } from "./services/user/user-service.js";
  import { PostService } from "./services/post/post-service.js";
  • Use dynamic imports for code splitting:
  // Good: Lazy load heavy modules
  const loadChartLibrary = () => import("./chart-library.js");

  // Avoid: Loading everything upfront
  import ChartLibrary from "./chart-library.js";

Common Errors and Solutions

SyntaxError: Cannot use import statement outside a module

// Problem: Missing type="module" in HTML
<script src="app.js"></script>

// Solution: Add type="module"
<script type="module" src="app.js"></script>

Module not found errors:

// Problem: Incorrect path
import { add } from "./math"; // Missing .js extension

// Solution: Include file extension
import { add } from "./math.js";

Cannot access before initialization:

// Problem: Circular dependency
// user.js
import { Post } from "./post.js";
export class User {
  createPost() {
    return new Post(); // Error if Post tries to import User
  }
}

// Solution: Use dynamic imports or restructure
export class User {
  async createPost() {
    const { Post } = await import("./post.js");
    return new Post();
  }
}

Mixed module systems:

// Problem: Mixing CommonJS and ES modules
const fs = require("fs"); // CommonJS
import path from "path"; // ES modules

// Solution: Use only ES modules
import fs from "fs";
import path from "path";

Debugging Tips

  • Check Network Tab: Open DevTools → Network → See if your JS files are loading.
  • Use Console: Add console.log statements to see if your code is running as expected.
  • Check File Extensions: Always include .js in your import paths.
  • Check Paths: Make sure the file paths (./, ../) are correct.
  • Modules Are Cached: Once a module is loaded, it won’t run again if imported elsewhere, it’s reused.

Performance Tips

  • Tree Shaking: Use named exports so unused code can be removed when building the project.
  • Code Splitting: Use dynamic imports for lazy loading (load code only when needed).
  • Keep It Small: Avoid importing entire libraries when you only need one small function from them.
  • Avoid Circular Imports: Try not to have files importing each other, it can cause bugs and make code harder to optimize.

Wrapping Up

That’s all for today!

I hope this post helps you.

For paid collaboration connect with me at : connect@shefali.dev

If you found this post helpful, here’s how you can support my work:
Buy me a coffee – Every little contribution keeps me motivated!
📩 Subscribe to my newsletter – Get the latest tech tips, tools & resources.
𝕏 Follow me on X (Twitter) – I share daily web development tips & insights.

Keep coding & happy learning!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert