Why imports are important in JS

My first foray into programming was writing Python on a Raspberry Pi to flicker some LED lights — it wasn’t much, but it taught me the basic guiding principles of software development. Right from the beginning, they were summarized in the Zen of Python:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.

These principles are so logical and sensible that they’ve started to guide the development of other languages. Now as a JavaScript developer, I can see how they’re reflected in standards like ES2015. Take the import and export statements for example: they’re simple, explicit, practical, and easy to explain. But why is that? Why not stick with the old require? Why this syntax instead of something more powerful? Why are imports so important in JavaScript? Let’s tackle those questions in this article.

Simple is better than complex

The first reason why imports are so important in JavaScript is because they allow us to split code into different modules and reuse it everywhere. It’s much simpler and easier to understand what’s going on when we organize our code this way.

Imagine if you wanted to use GraphQL in your Node.JS server, but the docs told you to copy and paste the GraphQL code directly into your main app.js! Without a way of compartmentalizing that GraphQL code into its own separate place, it would be too impractical to even exist. Frankly, that logic applies to most of our modern web development tools — they’re large and clunky and require massive teams to create and maintain, so if they didn’t have their own space to work in, it wouldn’t be practical to build them at all. So next time you load in the newest tool or framework, you have the underlying hero import to thank.

Readability counts

The import statement and its associated syntax from ES2015 is also known for being much easier to read than its predecessor require(). See for yourself:

import {tom} from './heartbreakers.js';
var {tom} = require('./heartbreakers.js')

Which of these seems more readable to you? Read them out loud — which sounds more like an English sentence? Import Tom from Heartbreakers JS or Var Tom equals require Heartbreakers JS?

We can still rename those imported variables just like if we had used require:

import {singer as jeff} from './electricLightOrchestra.js';

This is still just as readable as before, while still respecting the name of the variable that the file we’re importing is sending us. If electricLightOrchestra.js exports a variable called singer, which doesn’t fit the general scheme that we’re going for with our variable names, we can just explicitly rename it to jeff.

Sometimes a module only has one logical export. While we generally avoid this, it’s possible to just import the default export from that file. For example, if we had a JS file where we defined Bob Dylan, it wouldn’t really make sense for anybody else to be defined there, so we could just mark him as the default export and import him in our main file like this:

import './bobDylan.js';

Imports work exactly how you’d expect them to work.

It also matters that our code is easily readable by machines — if our build tool has a solid understanding of what files we’re importing and where, it can precompile all of the necessary code (and no more), shipping only the code we’ll actually use. For example:

import {george} from './beatles.js';

Our build tool will scan for this line, copy just the definition of george into the current file, and ditch whatever else was in beatles.js (presumably the definitions of john, paul, ringo, and depending on your taste, otherGeorge or brian). Assuming those definitions are all the same size and you’re only canonizing four of the Beatles, we just cut the size of the code in this file to 25% of what it was. The bigger the file, the more you save by specifying which piece of it you actually need. This is called “tree-shaking”, since it’s basically the software version of shaking a fruit tree so the useless ones fall off.

Practicality beats purity

That’s not to say that everything about the import syntax is easy to read; a few compromises have been made in the name of practicality. For example, those brackets aren’t intuitive to everyone, and can seem a little arbitrary. But it lets us treat our module like an object, and the things it exports as the object’s properties. If we think about it that way, then the bracket syntax might become slightly more intuitive to those who already make heavy use of concepts like object destructuring, and it lets us imagine the module exports as if they already have names.

Another sacrifice made to be flexible is the *. This is another thing we like to avoid because it defeats the whole point of tree-shaking, but you can do something like this:

import * as roy from './royOrbison.js';

All of the exports from ./royOrbison.js will be bundled into one object called roy. This is generally frowned upon because our build tool won’t know which of the exports from that file we’ll actually use — now it has to bundle all of them with our code just to be safe. Luckily, ./royOrbison.js only exports the one object; we’ll just have to refer to it as roy.roy now because that export was stuffed into an object also named roy. That potentially confusing and misleading result is why we don’t use this much: it’s hard to explain, and if the implementation is hard to explain, it's a bad idea.

If we’ve given up on tree shaking, then we could also use dynamic imports. Our build tool definitely isn’t going to be able to tell what it needs to bundle with our code, so this is going to break that whole process, but on rare occasions it can be useful to do this:

// define the variable youWantToIncludeTheDrummer dynamically above
if (youWantToIncludeTheDrummer) {
	const jim = await import('./sessionMusicians.js').jimKeltner;
	// note the "await" - import() returns a promise
	// do this either in an async function, or use the .then and .error functions

This is a nice-to-have when we need to import something dynamically, but it’s rarely useful enough to overcome the lack of tree-shaking. As a general rule of thumb, special cases aren't special enough to break the rules.

Namespaces are one honking great idea

You might’ve noticed that our variable naming scheme is a little rudimentary — surely there are other georges and bobs in the world of music, right? You’re right that they exist (we had an otherGeorge in the ./beatles.js file above, for one example), but in advance we’ve determined that this particular file isn’t going to contain any duplicate names. We actually can form our lineup without any overlap here:

export {
	roy.roy as roy, // remember, we imported roy using the weird asterisk syntax
	jim // assuming we actually did import jim, this is the tricky part to dynamic imports

We never have to worry about Jeff Beck or Bob Seger muddying up our namespace because we never imported them in the first place. Our five or six Traveling Wilburys are isolated to this file with the names we gave them here, removing any possible confusing duplicate variable errors.

This might seem like a given when we’re talking about bands (which generally function as independent units), but when we’re talking about software functionality, the lines between the modules sometimes get blurry. It’s not uncommon for what once was a small encapsulated file with a single function to mushroom into a 4000-line mess just because all of those additions felt logical as we made them.

In that case, it’s important to go back and decide what really belongs in that file, what we can move into their own files (and namespaces), and what really needs to be imported and made available to the rest of our program. Keeping the file size down reduces the time spent adding new features, fixing old bugs, and optimizing our code for performance and maintainability because we can much more easily grasp the space we’re working in when the file is only a handful of related functions as opposed to a third of our company’s infrastructure.

Wrapping up

While the Zen of Python isn’t a direct set of guidelines for JavaScript, it definitely lays out what good software architecture looks like, and the fact of the matter is, JavaScript’s modern import and export syntax checks all the boxes. It’s explicit, readable (for us and the computer), practical, unambiguous, and it encourages proper use of namespaces. It’s a win-win-win-win-win.

And if by chance, you’re looking for something fun and useful to import, check out Algolia’s JS client. Now is better than never, so don’t hesitate to reach out on Twitter if you’ve got questions.

About the authorJaden Baptista

Jaden Baptista

Technical Writer

Recommended Articles

Powered by Algolia AI Recommendations

Why imports are important in JS

Why imports are important in JS

Jaden Baptista

Jaden Baptista

Technical Writer
Good API Documentation Is Not About Choosing the Right Tool

Good API Documentation Is Not About Choosing the Right Tool

Maxime Locqueville

Maxime Locqueville

DX Engineering Manager
Introducing our new navigation

Introducing our new navigation

Craig Williams

Craig Williams

Director of Product Design & Research