ES6 Modules

Introduction to ES6 Modules

In the earlier days of web development, JavaScript usage on websites was minimal, primarily addressing minor functionalities. This scenario involved handling smaller JavaScript code blocks, often lacking complexity.

However, the landscape of web development has evolved significantly. Presently, we encounter web applications entirely crafted using JavaScript, necessitating a structured approach to managing and maintaining code. This shift led to the critical need for code modularity.

Initially, modules were a feature exclusive to environments like Node.js. Yet, with advancements in web technologies, modern browsers have embraced native support for modules. This pivotal change was ushered in by the introduction of ES6 modules.

ES6 modules revolutionize the way we handle JavaScript code. They enable developers to seamlessly import and export code elements, such as variables and functions, using import and export keywords. This feature greatly enhances code organization and reusability.

Here’s a straightforward illustration of employing ES6 modules through import and export:

javascript
	// Importing the 'addNumbers' function from a module
	import { addNumbers } from './math.mjs';
	
	// Using the imported function
	const result = addNumbers(10, 10);
	
	// Exporting the result for use in other modules
	export { result };

This example encapsulates the essence of ES6 modules, showcasing how they facilitate code segmentation and collaboration in modern JavaScript development.

Setup

Before we begin to use modules, we need to look at some minor setup that needs to be done.

Naming of module files

It’s the convention to use the .mjs extension for JavaScript module files instead of the .js extension e.g.

bash
	myModule.mjs

It is not mandatory to name a module file with the .mjs extension, but it is becoming common practice and is recommended. This is because:

  1. It lets developers know that the file they are working with is a module instead of a normal script.

  2. It ensures the file is correctly parsed as modules when tools are used, such as Babel.

Importing of Module files in a browser

To make use of a module in a browser, you need to add type="module" when importing the JavaScript module file.

Below is how we would usually import a normal JavaScript file:

html
	<script src="myFile.js"></script>

Below is how we would import a JavaScript module.

html
	<script type="module" src="myModule.mjs"></script>

If you tried to use import or export and did not specify type="module", you would likely get the following error:

txt
	Uncaught SyntaxError: Cannot use import statement outside a module (at main.mjs:1:1)

Imports and exports

import and export keywords are the way you will be able to use modules throughout your code.

When we use import we will be importing code from a module so we can put the code in another file. This code can be functions or variables. This code has to be exported from another file; otherwise, we won’t be able to import it.

When we use export we will be exporting code from a module so that it can be imported into other files using the import keyword.

Named and default imports/exports

There are two main ways we can use the import and export keywords, which are “named imports/exports” and “default imports/exports”.

We are going to look at the two different types below:

Named imports/exports

Named imports/exports allow us to import and export specific portions of our code.

For example, we might have three functions in a module, but we only want to expose two of them to other developers. In this case, we would then only export the two functions, e.g.

javascript
	// module.mjs
	function functionOne() {
	  // code here
	}
	
	function functionTwo() {
	  // code here
	}
	
	function functionThree() {
	  // code here
	}
	
	// We only export functionOne and functionTwo
	export { functionOne, functionTwo };

When we want to import these functions into another file, we will use the following code.

javascript
	// main.mjs
	import { functionOne, functionTwo } from './module.mjs';

If we tried to import functionThree like this, then it will throw an error:

javascript
	import {functionOne, functionTwo, functionThree};

The error will look as follows:

txt
	Uncaught SyntaxError: The requested module './module.mjs' does not provide an export named 'functionThree' (at main.mjs:1:36)

This is because we didn’t export functionThree from the module.mjs file.


Aliasing

When using “named imports/exports”, we have to use the name of the function or variable we are importing. If we use a named export to export the function named functionOne, we need to import it as this name. This is why it’s called a “named import/export”.

There is a way around this, though, by using “aliases”. This lets us change the named import. We do this by adding as after the variable name and then the name you want to use.

In the example below, we alias functionOne to have the name newFunctionName:

NOTE: When you use an alias, you can’t use the original name where you’ve added the alias.

javascript
	import { functionOne as newFunctionName } from './module.mjs';
	
	// ✅ This works
	newFunctionName();
	
	// ❌ This won't work as we have aliased 'functionOne' to be 'newFunctionName'.
	functionOne();

Default imports/exports

The other way we can import/export is using “default imports/exports”. The default import and default export are mechanisms for exporting and importing modules, specifically when you want to export or import a single entity (such as a function, class, or object) as the default. They provide a way to structure code, particularly in modern JavaScript (ES6+), for better maintainability and modularity.

When we talk about maintainability, we are referring to how easy it is for other developers to maintain and update the code. Always remember that in most cases the code you write is not just for yourself, but for other developers that you are working in a team with too.

When we talk about code modularity, we mean breaking down a large functions into smaller, independent pieces (modules) that each perform a specific task.

Default exports involve exporting code using the default keyword after the export keyword, e.g.

javascript
	function mainFunction() {
	  // code here
	}
	
	function subFunction() {
	  // code here
	}
	
	export default mainFunction;

In the above example, mainFunction() is exported as the default entity of the module. The subFunction() is not exposed to the outside modules as it does not have the export keyword in front of it.

When we import a “default export”, we don’t use the {} braces, we simply write the name we want to use:

javascript
	import mainFunction from './module.mjs';

Default imports can be named anything

You can name your default import whatever you would like. You do not have to use the same name like you do with “named imports/exports”. Although in the same breath - the more explicit you are, the better.

In the example below, we default export a function functionOne but we import it as newFunctionName:

javascript
	// module.mjs
	
	function functionOne() {
	  // code here
	}
	
	// We are using a default export, not a named export
	// You can tell by the 'default' keyword and lack of
	// {} curly braces
	default export functionOne;
javascript
	// main.mjs
	
	// Here we import the default export from 'module.mjs'
	// and called it 'newFunctionName'
	import newFunctionName from './module.mjs';

Combining named exports/imports with default exports/imports

You can combine named and default imports/exports, which is not uncommon.

We are using both named and default imports/exports in the example below:

javascript
	// module.mjs
	
	function functionOne() {
	  // code here
	}
	
	function mainFunction() {
	  // code here
	}
	
	// Named export
	export { functionOne };
	
	// Default export
	export default mainFunction;
javascript
	// main.mjs
	
	// Here we are using the default import (lack of curly braces)
	// as well as the named import { functionOne }
	import renamedDefaultFunction, { functionOne } from './module.mjs';

If you are familiar with React, you will likely have already seen default and named exports being used together:

jsx
	import React, { useEffect, useState } from 'react';

Other ways to export default and named exports

So far, you have only seen exports used in one way, where we add the exports at the bottom of the file.

javascript
	function myFunction() {
	  // code here
	}
	
	export { myFunction };

Another way we can export our code is to add the export keyword when you create the function. The advantage of this is that you don’t have an extra line of code that you need to keep updating as you add more code.

Named export:

Below is another way we could do a named export:

javascript
	export function myFunction() {
	  // code here
	}

Default export:

Below is an alternative way to do a default export:

javascript
	export default myFunction() {
	  // code here
	}

Splitting out code

As mentioned previously, modules allow us to split our code easily. We can choose what code (such as functions and variables) we can export from a module.

This means we can create a “separation of concerns”, which means that we have different modules for different parts of our application.

For example, if we had a game, we could have our player code in a player.js file, our enemy code in an enemy.js file and our main game in a game.js file.

This makes your code more manageable as your code is independent. Any changes to the player.js file will not have side-effects that impact your enemy.js file.

There are additional benefits to this. When you start using a bundler, such as Webpack, code-splitting lets you load only certain files that would be needed, allowing your website to load faster.

Practical example

NOTE: The code for this practical example can be found in the following repo.

In this example, we will emulate a part of a simple online shop. There is an item with a set price that will have tax added. It will be formatted, and the final amount will be displayed to the user.

Here is the list of files:

  1. index.mjs: This is our main entry point file that will call the functions from the modules.

  2. tax.mjs: This contains a function calculateTax which calculates the tax.

  3. utils.mjs: This contains a function formatCurrency which will make our item look more presentable e.g. 115 becomes “115.00 kr”.

  4. display.mjs: This contains the function displayAmount which will display a message to the user about how much the item will cost after tax is applied.

1. index.mjs

javascript
	// 1. index.mjs
	
	import { calculateTax } from './tax.mjs';
	import { formatCurrency } from './utils.mjs';
	import { displayAmount } from './display.mjs';
	
	// Our item initially costs 20
	const price = 100;
	const taxPercentage = 15;
	
	// We need to add tax to our item.
	const priceWithTax = calculateTax(price, taxPercentage);
	// priceWithTax = 115
	
	// We need to now format the item amount
	// so it has 2 decimal spaces but also so
	// it shows a currency symbol
	const formattedPriceWithTax = formatCurrency(priceWithTax, 'kr');
	// formattedPriceWithTax = '115.00 kr'
	
	// We finally display a message to the user:
	displayAmount(formattedPriceWithTax);
	// Logs:
	// The item costs 115.00 kr.

2. tax.mjs

javascript
	// 2. tax.mjs
	
	/**
	 * Calculates the tax for a given amount. Tax
	 * defaults to 15%.
	 * @param {number} amount
	 * @param {number} taxPercentage
	 */
	export function calculateTax(amount, taxPercentage = 15) {
	  return amount + amount * (taxPercentage / 100);
	}

3. utils.mjs

javascript
	// 3. utils.mjs
	
	/**
	 * Formats an amount to be a currency display amount e.g.
	 * "500" becomes "500.00 kr"
	 * @param {number} amount Currency amount
	 * @param {string} currencySymbol The currency symbol
	 * @returns
	 */
	export function formatCurrency(amount, currencySymbol = 'kr') {
	  const formattedAmount = amount.toFixed(2);
	  return `${formattedAmount} ${currencySymbol}`;
	}

4. display.mjs

javascript
	// 4. display.mjs
	
	/**
	 * Displays the cost of the item with tax applied to the user
	 * @param {string} amount
	 */
	export function displayAmount(formattedAmount) {
	  console.log(`The item costs ${formattedAmount}.`);
	}

Additional Info

Modules are deferred by default

When using a <script> tag with a normal JavaScript file, you run into the issue where the file loads before the HTML elements have been added to the page. This leads to errors when trying to select the HTML elements with JavaScript because these HTML elements don’t exist when the JavaScript is running.

The solution to this is to add defer or add the <script> import right before the end of the </body> tag.

Modules, on the other hand, are deferred by default. This means that you can add your module imports to the <head> and without needing to use the defer keyword.

Dynamic imports

You don’t have to load all of your modules upfront. Instead, you can load them when needed.

Below is an example of a dynamic import:

javascript
	// math.mjs
	
	export addNumbers(a, b) {
	  return a + b;
	}
javascript
	// main.mjs
	async function doSum() {
	  const mathModule = './math.mjs';
	
	  const { addNumbers } = await import(mathModule);
	
	  const result = addNumbers(10, 10);
	  console.log(result);
	  // Logs:
	  // 20
	}
	
	doSum();

Video

ES6 Modules:

## Additional Resources

V8.dev: Modules - Dynamic imports


Lesson Task

Brief

Develop a simple web application utilizing ES6 modules. Your application will consist of a main module that imports functions from two separate modules and uses them to perform tasks.

Requirements:

  1. Module 1 (stringUtils.mjs): Create a module that exports two functions:

    • capitalize: Takes a string and returns it with the first letter capitalized.
    • lowerCase: Takes a string and converts it to lowercase.
  2. Module 2 (mathUtils.mjs): Create a module that exports a function:

    • add: Takes two numbers and returns their sum.
  3. Main Module (app.mjs): Import the functions from the above modules and use them to:

    • Capitalize a string.
    • Convert a string to lowercase.
    • Add two numbers.
  4. Display the results of these operations in the console.

Expected Outcome:

On running the main module, it should display the results of the operations in the console.

Solution:

File: stringUtils.mjs

javascript
	// Exporting a function to capitalize the first letter of a string
	export function capitalize(str) {
	  return str.charAt(0).toUpperCase() + str.slice(1);
	}
	
	// Exporting a function to convert a string to lowercase
	export function lowerCase(str) {
	  return str.toLowerCase();
	}

File: mathUtils.mjs

javascript
	// Exporting a function to add two numbers
	export function add(num1, num2) {
	  return num1 + num2;
	}

File: app.mjs

javascript
	// Importing functions from the stringUtils and mathUtils modules
	import { capitalize, lowerCase } from './stringUtils.mjs';
	import { add } from './mathUtils.mjs';
	
	// Using the imported functions
	const capitalizedString = capitalize('hello');
	const lowerCaseString = lowerCase('WORLD');
	const sum = add(10, 5);
	
	// Displaying the results in the console
	console.log(`Capitalized: ${capitalizedString}`); // Output: Hello
	console.log(`Lowercase: ${lowerCaseString}`); // Output: world
	console.log(`Sum: ${sum}`); // Output: 15

Task for Students:

Create the above modules and run the main module (app.mjs). Ensure your environment supports ES6 module syntax. Observe the outputs in the console and understand how the import and export statements are used to share functionality across different modules.