JavaScript Modules: Past to Present

Published on
-
10 mins read
Authors

Many of you must have seen different ways of imports and exports in JavaScript. If you have worked with React or similar frameworks then import something from './abc.js' and export default something.
And while working with Node.js projects mostly the require and module.export syntax. So, let's try to understand what are they and why do they exist in the first place!

But why do we need modules?

You create modules to better organize and structure your codebase. You can use them to break down large programs into smaller, more manageable, and more independent chunks of code which carry out a single or a couple of related tasks.

  • Modules maximize reusability since a module can be imported and used in any other module that needs it. Also, for example NPM package could be created out of it to help use it in various projects.
  • Modules help with isolation which allows multiple people to work individually on the application/project. Also if one of the modules breaks, instead of replacing the whole app, you just have to replace the individual module that broke."
  • Modules makes your application code organized. Also prevent you from polluting the global name, avoiding naming collisions (you will understand this in later part of the post).

Okay, now let's dive in!!

First let us write some scripts & embbed them into one html document -

tasks.js
var taskList = ['Task One', 'Task Two', 'Task Three'];

function getTasks() {
  return taskList;
}
main.js
function appendTask(task) {
  const element = document.createElement('li');
  const text = document.createTextNode(task);
  element.appendChild(text);

  document.getElementById('tasks').appendChild(element);
}

document.getElementById('submit').addEventListener('click', function () {
  var input = document.getElementById('input');
  appendTask(input.value);

  input.value = '';
});

var tasks = window.getTasks();
for (var i = 0; i < tasks.length; i++) {
  appendTask(tasks[i]);
}
index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Tasks List</title>
  </head>

  <body>
    <h1>Tasks List:</h1>
    <ul id="tasks"></ul>

    <input id="input" type="text" placeholder="Enter new task" />

    <button id="submit">Submit</button>

    <script src="tasks.js"></script>
    <script src="main.js"></script>
  </body>
</html>

In above code example, we just have separated the javascript logic into 2 files and have added them as scripts into the document. You can check that the taskList, getTasks, tasks, appendTasks are available globally (i.e. accessbile on window object for browsers).

But this file separation doesn't give us modules as everything is available globally on the window object. So, How to convert this code into modules?
Ummm... What if instead of having our entire app live in the global namespace, we instead expose a single object, called APP. We can then put all the methods our app needs to run under the APP, which will prevent us from polluting the global namespace. We could then wrap everything else in a function to keep it enclosed from the rest of the app.

app.js
var APP = {}
tasks.js
function tasksContainer() {
  var taskList = ['Task One', 'Task Two', 'Task Three'];

  function getTasks() {
    return taskList;
  }

  APP.getTasks = getTasks;
}

tasksContainer();
main.js
function mainContainer() {
  function appendTask(task) {
    const element = document.createElement('li');
    const text = document.createTextNode(task);
    element.appendChild(text);

    document.getElementById('tasks').appendChild(element);
  }

  document.getElementById('submit').addEventListener('click', function () {
    let input = document.getElementById('input');
    appendTask(input.value);

    input.value = '';
  });

  var tasks = APP.getTasks();
  for (var i = 0; i < tasks.length; i++) {
    appendTask(tasks[i]);
  }
}

mainContainer();
index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Tasks List</title>
  </head>

  <body>
    <h1>Tasks List:</h1>
    <ul id="tasks"></ul>

    <input id="input" type="text" placeholder="Enter new task" />

    <button id="submit">Submit</button>

    <script src="app.js"></script>
    <script src="tasks.js"></script>
    <script src="main.js"></script>
  </body>
</html>

Now window object only has APP and our wrapper functions, tasksContainer and mainContainer. Notice that we're defining and then immediately invoking the wrapper functions. we can do this in a better way using the concept which we call - immediately Invoked Function Expression or IIFE in short.

IIFE

Here's how it will look like -

tasks.js
(function () {
  var taskList = ['Task One', 'Task Two', 'Task Three'];

  function getTasks() {
    return taskList;
  }

  APP.getTasks = getTasks;
})();
main.js
(function mainContainer() {
  function appendTask(task) {
    const element = document.createElement('li');
    const text = document.createTextNode(task);
    element.appendChild(text);

    document.getElementById('tasks').appendChild(element);
  }

  document.getElementById('submit').addEventListener('click', function () {
    let input = document.getElementById('input');
    appendTask(input.value);

    input.value = '';
  });

  var tasks = APP.getTasks();
  for (var i = 0; i < tasks.length; i++) {
    appendTask(tasks[i]);
  }
})();

Perfect! now only thing we can see on window object is APP which has the getTasks method.

So what are the benefits of this IIFE Module Pattern - First, we avoid dumping everything onto the global namespace. This will help with variable collisions and keeps our code more private.

Does it have any downsides ? It surely does. We still have one item on the global namespace, APP. If by chance another library uses that same namespace, we're in trouble. Also, you can notice that order of the <script> tags in our index.html file matter. If you don't have the scripts in the exact order they are now, the app wont work.

Cool, now we surely have made some progress with IIFE module pattern. Let's move ahead!

In JavaScript, Each file is its own module. Now we just need explicit imports & exports for the interactions between these files.

CommonJS :-

If you've used Node before, then CommonJS syntax should look familiar to you. Because Node.js uses CommonJS specification out of the box. We use require and module.exports syntax for importing and exporting the stuff.

Let's see -

tasks.js
var taskList = ['Task One', 'Task Two', 'Task Three'];

function getTasks() {
  return taskList;
}

module.exports = {
  getTasks: getTasks,
};
main.js
const { getTasks } = require('./tasks');

function appendTask(task) {
  const element = document.createElement('li');
  const text = document.createTextNode(task);
  element.appendChild(text);

  document.getElementById('tasks').appendChild(element);
}

document.getElementById('submit').addEventListener('click', function () {
  let input = document.getElementById('input');
  appendTask(input.value);

  input.value = '';
});

var tasks = getTasks();
for (var i = 0; i < tasks.length; i++) {
  appendTask(tasks[i]);
}

If you open your index.html file in browser, you should see error in the console mentioning caught ReferenceError: module is not defined. Because, unlike Node.js, browsers don't support CommonJS. So how do we make this syntax work in browsers?

Solution is using Module Bundlers like Webpack which looks at all the imports and exports, then intelligently bundles all of your modules together into a single file that the browser can understand. Then instead of including all the scripts in your index.html file and worrying about what order they go in, you include the single js file the bundler creates for you.

At this point, it should be pretty clear that modules are a critical feature for writing scalable, maintainable JavaScript code. It was now obvious that JavaScript needed a standardized, built in solution for handling modules. This is where the ES Modules comes into the picture.

ES Modules :-

ES Modules are now the standardized way to create modules in JavaScript. Let's take a look at the syntax.

To specify what should be exported from a module you use the export keyword.

accounts.js
var accounts = [
  {
    holderName: 'Person1',
    balance: 1000,
  },
  {
    holderName: 'Person2',
    balance: 3000,
  },
  {
    holderName: 'Person3',
    balance: 700,
  },
];

export function getBalance(holderName) {
  var account = accounts.find((account) => account.holderName === holderName);
  return account.balance;
}

function getTotalBankDeposits() {
  let depositSum = 0;
  accounts.forEach((account) => (depositSum += account.balance));

  return depositSum;
}

Note: we are only exporting getBalance method, so accounts data & getTotalBankDeposits wont be available for others to import.

Now to import getBalance you have a two different options. One is to import everything that is being exported from accounts.js.

import * as accounts from './accounts.js'

let balance = accounts.getBalance('Person1')

console.log("Person1's account balance is: ", balance)

Second option is using named imports -

import { getBalance } from './accounts.js'

let balance = getBalance('Person1')

console.log("Person1's account balance is: ", balance)

With ES Modules you can specify multiple exports, And can also specify one default export.

export default function getAccountDetails(holderName) {
  let account = accounts.find((account) => account.holderName === holderName)

  return account
}

And then import it like this -

import getAccountDetails from './accounts.js' // any name can be give to this import, not just the "getAccountDetails".

Now, what if you had a module that was exporting a default export but also other regular exports as well?

accounts.js
var accounts = [
  {
    holderName: 'Person1',
    balance: 1000,
    type: 'Saving',
  },
  {
    holderName: 'Person2',
    balance: 3000,
    type: 'Current',
  },
  {
    holderName: 'Person3',
    balance: 700,
    type: 'Saving',
  },
];

export function getBalance(holderName) {
  var account = accounts.find((account) => account.holderName === holderName);
  return account.balance;
}

function getTotalBankDeposits() {
  let depositSum = 0;
  accounts.forEach((account) => (depositSum += account.balance));

  return depositSum;
}

export default function getAccountDetails(holderName) {
  let account = accounts.find((account) => account.holderName === holderName);

  return account;
}

Now, this is how the import syntax would look like -

import getAccountDetails, { getBalance } from './accounts.js'

Because ES Modules are now native to JavaScript; modern browsers support them without using a bundler. Let's look back at our simple task list example and see what it would look like with ES Modules.

tasks.js
let taskList = ['Task One', 'Task Two', 'Task Three'];

export default function getTasks() {
  return taskList;
}
main.js
import getTasks from './tasks.js';

function appendTask(task) {
  const element = document.createElement('li');
  const text = document.createTextNode(task);
  element.appendChild(text);

  document.getElementById('tasks').appendChild(element);
}

document.getElementById('submit').addEventListener('click', function () {
  var input = document.getElementById('input');
  appendTask(input.value);

  input.value = '';
});

let tasks = getTasks();
for (var i = 0; i < tasks.length; i++) {
  appendTask(tasks[i]);
}

Now in index.htmlfile, all we need to do is include only the main file and add type='module' attribute to the script tab.

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Tasks List</title>
  </head>

  <body>
    <h1>Tasks List:</h1>
    <ul id="tasks"></ul>

    <input id="input" type="text" placeholder="Enter new task" />

    <button id="submit">Submit</button>

    <script type="module" src="main.js"></script>
  </body>
</html>

Outro :-

Because ES Modules are static, import statements must always be at the top of a module. You can't conditionally import them. Because of which the loader can now statically analyze the module tree, figure out which code is actually being used, and drop the unused code from your bundle. This is what we call as Tree Shaking or Dead Code Elimination.

That's it! Hope you found this post interesting and now have the better understanding about modules & import-export syntaxes in JavaScript.

Adios