Modules 101: how and why of exporting and importing in JavaScript
Written in the Autumn 2018
Published 29 December 2021
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Can you imagine finding the information you want if all of Wikipedia was on a single page? I can't either.
The same principle applies in programming. Rather than having one massive file with all the information, the application's code is divided into files and folders. They are structured in a way that enables newcomers to the codebase to easily find the information they are after. It also makes the codebase easier to maintain.
In most module standards, JavaScript code is scoped to the file that holds it (the "origin" file). If we want to use that code elsewhere (in "destination" file), we need to explicitly export it from the origin file, and import it in the destination file. There are several ways to do it in JavaScript, the imports and export methods are not interchangeable, however! (Ok, you might be able to make it work, but it would be like trying to put a square peg in a round hole)
This post will explain the different methods and provide examples. All the code here is available in this repo so that you can can see various approaches on the same code. Please note that the repo is only there to demonstrate the structure - don't expect the code there to work "as is". The specific paths mentioned in this article refer to the actual paths in the project. We'll be looking at: the basic four mathematics methods: additions, subtraction, multiplication, and division; how the functions can be arranged in a project; and how to import and export them so they are accessible from a single file.
This post will also explain, in a bit more depth, why it is important to use import and export functionality, and provide some historic background to why things are as they are now.
We will discuss possible structures first.
All-in-one is, as the name implies, the structure where all the functions live in a single file. This isn't necessarily bad per se. If your project only has a couple of functions, there is no need to over-complicate things. Keep it simple.
Direct importing involves the functions being exported from their origin file, imported and used by the destination file. This structure is well suited to medium-small projects, especially where one function is considerably larger than the others.
Indirect importing is used in medium to large projects where semantic separation into folders and sub-folders makes logical sense and helps, rather than hinders, to find the relevant code.
But WHY?
Ok, Mx I'll-have-Wikipedia-on-one-page. Each to their own. However, if you are working as part of a group, there are a handful of concepts that are worth following. It will make your life easier in the long run, I promise.
The concept of separation of concerns is what it says on the tin. If you have a bunch of code that's responsible for the Thing, move it into a separate file so that you can reuse it easily. It improves clarity and makes it obvious that the Thing is being dealt with. It significantly reduces the likelihood that others (including future you) will waste time implementing functionality that already exists.
And that brings us to coupling and cohesion. Those terms are repeated again and again in Computer Science courses. Programmers are encouraged to aim for low coupling and high cohesion in their codebase. There are a multitude of articles explaining the concepts. These are the ones that I've read: University of Washington, FreeFeast and Simple Programmer. Coupling is a fancy technical term for "independent" (nota bene low coupling means highly independent). Cohesion is a fancy term for "logically self contained".
Bikes, computers, Node.js
What do bikes, desktop computers, and Node.js have in common? It sounds like a good start to a joke, but I can't think of a witty ending. What they have in common is what makes them great objects. They are all modular! Your bike bell breaks, you replace it with a new one. Same for a sound card in a desktop computer. You can place those objects on the coupling / cohesion axis, if you wish to.
If you've been working with JavaScript for a while, you are probably familiar with Node.js. It's a run-time environment that executes your JS code outside of the browser. Like the bicycle frame to a bike, or the motherboard to a desktop computer, Node.js is the one of the main components of JavaScript development (both on the back end, or in the front end build pipelines).
Because programmers are inherently lazy, rather than writing the same code many times, they bundle the code into a module that they can just import next time they need it. And because they are lovely human beings, they share their module on the internet for other people to use. If you use Node.js, it is highly likely that you are also familiar with npm or yarn; the most popular node package managers out there.
To actually use a module in your code, you first need to install it in your project, and then, import it inside the relevant file.
It is worth noting that even when you move some code into a different file to maintain separation of concerns, you are technically creating modules.
It wasn't always this way.
As all spoken languages do, programming languages (including JavaScript) keep evolving too. But unlike spoken languages, programming languages can't be adopting every single new thing without checking how it fits into a language on a larger scale.
Prior to ES6 (aka ES2015) JavaScript did not come with inbuilt methods to import and export modules. module.exports and require("./path") themselves only came with CommonJS; an API designed to make it easier to build (using JavaScript) apps that work outside of the browser (eg use JS server-side). A band-aid, if you will, to include much needed functionality without waiting for the next iteration of the JavaScript core. There have been other attempts to solve this problem, AMD being the one of note, but none made as big of an impact as CommonJS did.
But WHY?
Today JavaScript is used by a number of frameworks to make apps and websites, and there's Node.js to do "backend" logic. Let's pause there for a moment.
JavaScript was devised to be a programming language for the web; one that all browsers would support and enable the websites do magic. This has never changed. Node.js isn't some independent interpreter, it's simply the guts of Chrome browser responsible for handling JavaScript that have been repackaged to work headless. The V8 execution engine, as it is called, alongside with other feature such as threading, unified API, and package management, make up Node.js.
So if it's all browser, how did modules work before CommonJS? Well, it's all browser based, right? All all browsers have windows, right? So basically, ye ol' school modules were designed in such a way that they hooked up to the window.
Translate, transform, TRANSPILE!
Let's come back to now. You're setting up a new JavaScript project. You're up to speed with the ternary operator, rocket science, and all the swanky functionality of ES6. It's a no-brainer, really; you should code in ES6 and use its native module exporting / importing! Or is it?
You see, there is an issue with this. Except rare cases when you know the recipient is running the latest Chrome, when you ship your code, you don't actually ship the ES6 version. You need to transpile your code first, so that older browsers can render your product without mishaps. A transpiler takes your ES6 code and spits out its equivalent in ES5. The resulting code, although handled gracefully by vast majority of browsers, old and new, can also be heavier and slower than if written in ES5 (& CommonJS) to start with.
So before you start writing your new project, have a good think if you actually have a reason to use ES6 that needs transpiling. Written in late spring 2017, this article by FreeCodeCamp explains very well when the code should be transpiled. It boils down to TS, TS, TS, TS! (OK, there's some creative wrangling of words to make the acronym work throughout): (use of) TypeScript; (want to) Tree Shake; Trial and Study (latest features of the language); and (use of) Tags in Script (like JSX in React).
The tastiest fruit is the ripe one
I'm very fond of the vegetable patch in my back garden. Among all the tasty things, every year tomatoes get the prime spot (I can recommend German Johnson variety). Now, tomatoes have an interesting property; if they are not ready to be picked, they are quite difficult to get off the vine. If they are ripe, a gentle touch can set them free. Fruit trees are the same. So "tree shaking" gets you only the fruit you're interested in (ie the ripe ones).
As with many concepts in this blog post, this one merits from a bit of background explanation. Going back to the possible structures, both direct importing and indirect importing rely on a single entry point. It's what it says on the tin. Back when each module was attached to the window directly there were many entry points to your codebase. With the arrival of Browserify and Webpack this changed. Those tools bundled the code together by looking at main.js, gathering all the modules that were required, and shipped it in a neat little package for the browser to access. Although it was an improvement over chucking everything at the window, it came with a major drawback. Those tools were very eager, too eager in fact. They packaged each and every little part of the module, even if all you needed was, well, only a little part of it. In practice this means that you're shipping code that's never used, making your product heavier than it needs to be. You wouldn't come up to a tree, harvest every single fruit (ripe or not) and then sort them at home, would you?
Tree Shaking was first possible in ES6. Given the fruit tree example, you can guess what it does. If you only need a small part of the module, only the part that is actually needed is packaged and shipped. The resulting package is more compact, quicker to serve to the client, and easier to track metrics overall.
TL;DR
E: export
I: import
U: use
ES5
Export the only function from a file
I: var add = require("./add");
U: add(3,6);
Export a function when there are many in a file without changing its name
I: var multiply = require("./multiplyAndDivide").multiply;
U: multiply(3,6);
Export a function when there are many in a file, but assing it a different name in the process
I: var divideForMe = require("./multiplyAndDivide").howManyTimes;
U: divideForMe(3,6);
ES6
Export the only function from a file
I: import add from "./add";
U: add(3,6);
Export a function when there are many in a file without changing its name
I: import {multiply, divide} from ".multiplyAndDivide.js";
U: multiply(3,6);
Export a function when there are many in a file, but assing it a different name in the process
I: import * as mANDd from ".multiplyAndDivide.js";
U: mANDd.multiply(3,6);
Exporting and importing, CommonJS style.
CommonJS uses module.exports to export content from file, and var {name} = require("path/to/origin") to import code into the destination file.
When it comes to direct importing, the implementation can differ depending on how many functions you are exporting from a given file.
To export a function from a file that has only one function - "add", use module.exports = add;.
To import it, use var add = require("./add");
If you have more than one function that you want to export from a file, exporting is a little bit more complex. To export "multiply" from our "multiplyAndDivide.js" file you need to tell module.exports to look at a specific function: module.exports.multiply = multiply;. The semantics are as follows:
- module.exports. to initiate the export
- multiply: the word between the dot and the equals is the name you want this function to be known outside of the current file - a reference name.
- = assigns the export to…
- multiply: … the function's name in the current file
To import this into the destination file, you need to declare a variable, assign it to the path - as you did when exporting a single function AND you need to provide a reference to the function by adding .{referenceName}, like so: var multiply = require("./multiplyAndDivide").multiply.
A quick recap:
- Export/import a function from a file that only has that function: module.exports = add; & var add = require("./add");
- Export/import a function from a file that has many functions using the same name throughout: module.exports.multiply = multiply & var multiply = require("./multiplyAndDivide").multiply;
- Export/import a function from a file that has many functions using different name (a reference name): module.exports.howManyTimes = divide & var divide = require("./multiplyAndDivide").howManyTimes;
Indirect importing relies on importing the functions into a middle-man file, usually called index.js. You need to export and import functions into index.js the direct way. Then, you can bundle all the functions into an object like so:
add: add,
subtract: substract,
multiply: multiply,
divide: divide
}
and import it in the destination file the "direct" way.
Exporting and importing in ES6.
In some ways, exporting and importing in ES6 isn't that much different than in ES5. But the vocabulary is different. And some of ES6 nifty features are used. And suddenly it feels like it's a new and confusing concept. However, like most things in programming, there is logic behind the madness!
To export the only function in the file, you can either write the function and then export it: export default add; . Or you can export it in-line, so to speak: export default subtract = (a,b) => { return a-b}. When you export a function as default, it will be, well, the one which is referred to by default when the file is imported. To import it, use import add from "./add.js";.
The syntax for importing is as follows:
- import
- name of the function to import OR * to import all functions OR destructuring argument
- as
- name this function should be known as in the destination file
- from
- path to the origin file
OR? OR?! AS? AS?!… Breathe! There is logic behind the madness! Let's take the a look at the options one by one.
Importing by name of the function: this is basically exporting / importing the only function in the file. Use export default add; & import add from "./add.js"; when you declare and export the function in two distinct steps. To declare and export in one go, write export default = (a,b) => {return a-b}; & import subtract from "./subtract.js";
Importing all with a star: to export, you build an object of functions, like you did in ES5. However, ES6 has a nifty feature where the key is automatically assigned the value of the same name, so you can have: export default { multiply, divide } in the multiplyAndDivide.js, and import * as mANDd from "./multiplyAndDivide.js" in the destination file. as is used to give the package that is being imported a name that can be used to refer to it. That way, if you're importing all functions from many files, you always know which file you're referring to!
In this case, to call the multiply function with arguments of 5 and 3, you need to write mANDd.multiply(5,3); - the first part being the reference to the file that you're importing (`mANDd`) and the second is the name of the function you want to use.
Destructuring object that we're importing: another nifty feature of ES6 is destructuring. Rather than importing key-values from an object one by one, you can do it in a single line: import { multiply, divide } from "./multiplyAndDivide.js". If you do that, you can call those functions directly: multiply(5,3);.
Indirect importing is simpler in ES6 than in ES5. In ES5 you need to import each function into the index.js file, then assign them to an object. In ES6, you can export functions without importing them, like so:
export { default as Button } from './button';
export { default as Icon } from './icon';
export { default as Image } from './image';
export { default as Link } from './link';
export { default as Margin } from './margin';
export { default as Padding } from './padding';
export { default as Title } from './title';
The code snippet above is an example content of the index.js file in Components folder of a React project.
Conclusion
As you have learned, there are a number of different ways one can set up code modularisation. The implementation has evolved from an add-on to a native functionality of JavaScript and the ways of setting up modularisation reflect this.
Although this is not a new concept, it is taking time to implement at native level. ES Modules, which bring some great functionality to the table (eg Tree Shaking), are inscribed into ES6 standard, but it took a few years before the browsers started shipping Modules natively. At the time of writing, Modules in Node 11 have "Experimental" status. It will be an exciting and big step for JavaScript when it joins other, more mature coding languages, when the ES Modules will be fully supported out of the box!
Addendum - what about now?
I am finally getting round to publishing this in late December 2021. I might - might - get around to writing a paragraph or two about the current status of ES Modules and how they fit into the JavaScript ecosystem. But at this point it would be another thing that's stopping me from publishing this, so it will need to wait.
Acknowledgements
I would like to thank my colleagues for their help and support; brainstorming, proofreading, and patient explanations!
A note on mental health
I decided, wherever possible, to end my talks / write-ups with this note. Mental health is important. Depression and anxiety are real. They can suck out the joy of living out of you. There are many other mental health issues, but those are the two that affected me, so I talk about just those two.
Depression has many symptoms. Many say they lose interest in doing thing they previously loved doing. Getting out of bed can be difficult, standing in the shower might take up all the energy you have for the day. I found that I was always tired; stuck in first gear so to speak.
Anxiety can be debilitating. It stops you doing stuff you love for irrational reasons. I toyed with the idea of moving away from the town I was living in (and where I had all my support network) to pursue an MSc. Just the idea of the possibility of a chance of moving away triggered an hour long sobbing/crying episode.
Meds helped. Many mental health issues are simply chemical imbalances in the brain. "Meds are unnatural" approach is bullshit; if you had diabetes, you would not refuse insulin because "it's unnatural". Meds are there to compensate where your body falls short. Your mileage may vary, different meds will have different effect; if an average day was 4/10, Sertraline (first SSRI I tried, stayed on for about 6 months) made it 6/10 and Citalopram (second one, that I stayed on for better part of 2 years) made it 9.5/10 for me.
I'm no longer on meds. Circumstances in my life changed, I was able to scale back and stop completely. I still sometimes have bad days. I conside myself a spoonie. I often flop on the sofa at the end of the day unable to do much more than browse the internet aimlessly.
If you are struggling, don't be afraid to talk. Not everyone might be able to empathise. You might be surprised how many other people face the same difficulties. There are counselling services on-line that you can access for a reasonable price. If you can, talk to your doctor. Figure out what works for you.
Be honest with yourself; admitting something isn't right is the hardest step in making it right.
If you found this article useful, I'd love to know!
Find me on Twitter.