JavaScript ES Modules
In this article, we’ll have a look at ES Modules. Introduced in ES6, they have become the basis for code organization for new Javascript development.
In this section, we’ll quickly go over a few quirks you might encounter when working with them.
The main advantage of modules is that splitting code into independent chunks makes it easier to wrap your head around each one. This makes your code cleaner and your work more effective.
The import keyword
Before ES modules, code could be split into multiple files, and included using the require keyword in NodeJS.
However, this didn’t work in browsers, where code could only be included from the HTML — which had the big disadvantage of forcing you to bundle your code into a single file that could be loaded once.
Happily, ES modules are now supported in all modern browsers, so there is no reason not to use them!
To include code from one file into another, we can use the import keyword, which replaced the previous require function:
// Previous syntax
const { count } = require(“./math.js”);
// ES Modules syntax
import { count } from “./math.js”;
The interesting part is that all imports are evaluated first, before any code is run. They are basically hoisted.
This makes sure we don’t have circular dependencies in our code, and also imports modules only once, even though multiple files may need them. This makes modules singletons.
Live bindings
When you import a value from another module, what you get is called a live binding. This means that you can’t modify the value, but the original module can do so! Let’s look at an example:
// math.js
let count = 3;
setTimeout(() => count++, 500);
export { count };
// main.js
import { count } from “./math.js”;
setTimeout(() => console.log(count), 0); // 3
// The value changes
setTimeout(() => console.log(count), 1000); // 4
The math.js module was able to change the exported value of count, changing the value displayed in main.js.
An interesting thing happens when you start exporting objects. Since they are always copied by reference, you can change their properties when importing them.
// math.js
const constants = {
PI: 3.14159265359,
e: 2.71828182846,
};
export default constants;
// calculator.js
import CONSTANTS from "./math.js";
CONSTANTS.PI = 4;
export const square = (x) => x * x;
// main.js
import { square } from "./calculator.js";
import CONSTANTS from "./math.js";
console.log("PI^2 is", square(CONSTANTS.PI));
// PI^2 is 16
The calculator.js file modifies the value of PI, making the main.js print an incorrect value. We must therefore be careful when exporting objects!
Dynamic import
As we’ve seen, imports are resolved before the code is run, which allows the two steps to be run independently.
However, this also means that we can’t use variables in our imports, since they won’t be defined yet during the import resolution.
To fix this, we can use the so-called dynamic import, which is only called when the code runs.
To use it, simply call the import function:
// config.js
export const reportId = 3;
// main.js
import { reportId } from “./config.js”;
const data = import(`./report_${reportId}.js`); // will import report_3.js
Conclusion
There is much more to say about ES modules, but this should get you covered to understand their basic quirks.
If you want to go further, make sure to check out this deep dive on mozilla.org.