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
:
// 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.
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:
It lets developers know that the file they are working with is a module instead of a normal script.
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:
<script src="myFile.js"></script>
Below is how we would import a JavaScript module.
<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:
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.
// 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.
// main.mjs
import { functionOne, functionTwo } from './module.mjs';
If we tried to import functionThree
like this, then it will throw an error:
import {functionOne, functionTwo, functionThree};
The error will look as follows:
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.
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.
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:
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
:
// 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;
// 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:
// module.mjs
function functionOne() {
// code here
}
function mainFunction() {
// code here
}
// Named export
export { functionOne };
// Default export
export default mainFunction;
// 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:
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.
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:
export function myFunction() {
// code here
}
Default export:
Below is an alternative way to do a default export:
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:
index.mjs
: This is our main entry point file that will call the functions from the modules.tax.mjs
: This contains a functioncalculateTax
which calculates the tax.utils.mjs
: This contains a functionformatCurrency
which will make our item look more presentable e.g. 115 becomes “115.00 kr”.display.mjs
: This contains the functiondisplayAmount
which will display a message to the user about how much the item will cost after tax is applied.
1. index.mjs
// 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
// 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
// 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
// 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:
// math.mjs
export addNumbers(a, b) {
return a + b;
}
// 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 ResourcesV8.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:
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.
Module 2 (
mathUtils.mjs
): Create a module that exports a function:add
: Takes two numbers and returns their sum.
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.
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
// 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
// Exporting a function to add two numbers
export function add(num1, num2) {
return num1 + num2;
}
File: app.mjs
// 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.